diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1ae009b033..c7d3d8fa9c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,10 +10,18 @@ env: NODE_VERSION: 14 NODE_OPTIONS: --max_old_space_size=6144 +# Set default workflow permissions +# All scopes not mentioned here are set to no access +# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token +permissions: + actions: none + jobs: release: name: Release runs-on: ubuntu-latest + permissions: + contents: write # Required to upload release assets steps: - name: Checkout the repository uses: actions/checkout@v2 @@ -47,6 +55,13 @@ jobs: script/release + - name: Upload release assets + uses: softprops/action-gh-release@v0.1.14 + with: + files: | + dist/*.whl + dist/*.tar.gz + wheels-init: name: Init wheels build needs: release diff --git a/cast/src/receiver/second-load.ts b/cast/src/receiver/second-load.ts index e3c0885561..97de839a76 100644 --- a/cast/src/receiver/second-load.ts +++ b/cast/src/receiver/second-load.ts @@ -1,4 +1,3 @@ -import "web-animations-js/web-animations-next-lite.min"; import "../../../src/resources/ha-style"; import "../../../src/resources/roboto"; import "./layout/hc-lovelace"; diff --git a/gallery/public/images/clearspace.png b/gallery/public/images/clearspace.png new file mode 100644 index 0000000000..ffd1aa5f62 Binary files /dev/null and b/gallery/public/images/clearspace.png differ diff --git a/gallery/public/images/logo-variants.png b/gallery/public/images/logo-variants.png new file mode 100644 index 0000000000..2a35152ce5 Binary files /dev/null and b/gallery/public/images/logo-variants.png differ diff --git a/gallery/public/images/logo-with-text.png b/gallery/public/images/logo-with-text.png new file mode 100644 index 0000000000..c47eaa7112 Binary files /dev/null and b/gallery/public/images/logo-with-text.png differ diff --git a/gallery/public/images/logo.png b/gallery/public/images/logo.png new file mode 100644 index 0000000000..e3faf04c5a Binary files /dev/null and b/gallery/public/images/logo.png differ diff --git a/gallery/public/images/using-our-logo.png b/gallery/public/images/using-our-logo.png new file mode 100644 index 0000000000..0199a37b64 Binary files /dev/null and b/gallery/public/images/using-our-logo.png differ diff --git a/gallery/script/netlify_build_gallery b/gallery/script/netlify_build_gallery index a5732d4c83..db8be615d1 100755 --- a/gallery/script/netlify_build_gallery +++ b/gallery/script/netlify_build_gallery @@ -23,7 +23,7 @@ if [[ "${PULL_REQUEST}" == "true" ]]; then createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID" gulp build-gallery if [ $? -eq 0 ]; then - createStatus "success" "Build complete" "$DEPLOY_URL" + createStatus "success" "Build complete" "$DEPLOY_PRIME_URL" else createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID" fi diff --git a/gallery/sidebar.js b/gallery/sidebar.js index 02ffeb2aa8..14a8d38fe8 100644 --- a/gallery/sidebar.js +++ b/gallery/sidebar.js @@ -36,12 +36,17 @@ module.exports = [ category: "misc", header: "Miscelaneous", }, + { + category: "brand", + header: "Brand", + }, { category: "user-test", - header: "User Tests", + header: "Users", + pages: ["user-types", "configuration-menu"], }, { category: "design.home-assistant.io", - header: "Design Documentation", + header: "About", }, ]; diff --git a/gallery/src/components/demo-black-white-row.ts b/gallery/src/components/demo-black-white-row.ts index 549f10554c..43a58ea59e 100644 --- a/gallery/src/components/demo-black-white-row.ts +++ b/gallery/src/components/demo-black-white-row.ts @@ -53,13 +53,19 @@ class DemoBlackWhiteRow extends LitElement { firstUpdated(changedProps) { super.firstUpdated(changedProps); - applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), { - default_theme: "default", - default_dark_theme: "default", - themes: {}, - darkMode: true, - theme: "default", - }); + applyThemesOnElement( + this.shadowRoot!.querySelector(".dark"), + { + default_theme: "default", + default_dark_theme: "default", + themes: {}, + darkMode: true, + theme: "default", + }, + undefined, + undefined, + true + ); } handleSubmit(ev) { diff --git a/gallery/src/components/page-description.ts b/gallery/src/components/page-description.ts index 8e449195a6..e9680e59b3 100644 --- a/gallery/src/components/page-description.ts +++ b/gallery/src/components/page-description.ts @@ -12,7 +12,14 @@ class PageDescription extends HaMarkdown { if (!PAGES[this.page].description) { return html``; } + return html` +
+
+ ${PAGES[this.page].metadata.title || this.page.split("/")[1]} +
+
${PAGES[this.page].metadata.subtitle}
+
${until( PAGES[this.page] .description() @@ -25,9 +32,22 @@ class PageDescription extends HaMarkdown { static styles = [ HaMarkdown.styles, css` + .heading { + padding: 16px; + border-bottom: 1px solid var(--secondary-background-color); + } + .title { + font-size: 42px; + line-height: 56px; + padding-bottom: 8px; + } + .subtitle { + font-size: 18px; + line-height: 24px; + } .root { max-width: 800px; - margin: 0 auto; + margin: 16px auto; } .root > *:first-child { margin-top: 0; diff --git a/gallery/src/data/text.ts b/gallery/src/data/text.ts new file mode 100644 index 0000000000..f25d4933cd --- /dev/null +++ b/gallery/src/data/text.ts @@ -0,0 +1,11 @@ +export const LONG_TEXT = ` +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum. + +Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci. + +Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo. + +In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla. + +Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim. +`; diff --git a/gallery/src/ha-gallery.ts b/gallery/src/ha-gallery.ts index 86e64e8d35..4f22bac519 100644 --- a/gallery/src/ha-gallery.ts +++ b/gallery/src/ha-gallery.ts @@ -5,6 +5,7 @@ import { html, css, LitElement, PropertyValues } from "lit"; import { customElement, property, query } from "lit/decorators"; import "../../src/components/ha-icon-button"; import "../../src/managers/notification-manager"; +import "../../src/components/ha-expansion-panel"; import { haStyle } from "../../src/resources/styles"; import { PAGES, SIDEBAR } from "../build/import-pages"; import { dynamicElement } from "../../src/common/dom/dynamic-element-directive"; @@ -44,6 +45,10 @@ class HaGallery extends LitElement { for (const page of group.pages!) { const key = `${group.category}/${page}`; const active = this._page === key; + if (!(key in PAGES)) { + console.error("Undefined page referenced in sidebar.js:", key); + continue; + } const title = PAGES[key].metadata.title || page; links.push(html` ${title} @@ -53,10 +58,9 @@ class HaGallery extends LitElement { sidebar.push( group.header ? html` -
- ${group.header} + ${links} -
+ ` : links ); @@ -92,27 +96,34 @@ class HaGallery extends LitElement { ${dynamicElement(`demo-${this._page.replace("/", "-")}`)} @@ -186,27 +197,16 @@ class HaGallery extends LitElement { padding: 4px; } - .sidebar details { - margin-top: 1em; - margin-left: 1em; - } - - .sidebar summary { - cursor: pointer; - font-weight: bold; - margin-bottom: 8px; - } - .sidebar a { color: var(--primary-text-color); display: block; - padding: 4px 12px; + padding: 12px; text-decoration: none; position: relative; } .sidebar a[active]::before { - border-radius: 4px; + border-radius: 12px; position: absolute; top: 0; right: 2px; @@ -237,14 +237,32 @@ class HaGallery extends LitElement { .page-footer { text-align: center; - margin: 16px 0; - padding-top: 16px; - border-top: 1px solid rgba(0, 0, 0, 0.12); + margin: 16px; + padding: 16px; + border-radius: 12px; + background-color: var(--primary-background-color); + } + + .page-footer div { + margin-top: 4px; + } + + .page-footer .header { + font-size: 16px; + font-weight: 500; + line-height: 28px; + text-align: center; + } + + .page-footer .secondary { + line-height: 23px; + text-align: center; } .page-footer a { display: inline-block; margin: 0 8px; + text-decoration: none; } `, ]; diff --git a/gallery/src/pages/brand/logo.markdown b/gallery/src/pages/brand/logo.markdown new file mode 100644 index 0000000000..e931fed8af --- /dev/null +++ b/gallery/src/pages/brand/logo.markdown @@ -0,0 +1,34 @@ +--- +title: "Logo" +--- + +![Using our logo](/images/using-our-logo.png) + +# Using our logo + +As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color. + +[Download Logo](https://github.com/home-assistant/assets/tree/master/logo) + +![Logo](/images/logo.png) + + +## Using the icon + +Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon. + +![Logo variants](/images/logo-variants.png) + +## Using the right variant + +The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography. + +When needed you can use our logo without a shadow, as seen as the second variant. + +The outlined logo should only be used on packaging. + +## Exclusion zone + +The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon. + +![Clearspace](/images/clearspace.png) diff --git a/gallery/src/pages/brand/our-story.markdown b/gallery/src/pages/brand/our-story.markdown new file mode 100644 index 0000000000..5bdd3866e8 --- /dev/null +++ b/gallery/src/pages/brand/our-story.markdown @@ -0,0 +1,41 @@ +--- +title: "Our story" +--- + +## Open source home automation that puts local control and privacy first + +Home Assistant is a free and open-source software for home automation that is designed to be the central control system for smart home devices with a focus on local control and privacy. It can be accessed via a web-based user interface, via apps for Android and iOS, or using voice commands via a supported virtual assistant like Google Assistant and Amazon Alexa. + +IoT devices and services are supported by modular support for controlling proprietary ecosystems if they provide public access via an Open API for third-party integrations and protocols like Bluetooth, MQTT, Zigbee, and Z-Wave, After the Home Assistant software application is installed as a computer appliance it will act as a central control system for home automation. Information from all entities it sees can be used and controlled from within scripts trigger automations using scheduling and "blueprint" subroutines, e.g. for controlling lighting, climate, entertainment systems, and appliances. + +# Open Home + +The Open Home is our vision for the smart home. It defines the values that we put at the heart of every decision we make at Home Assistant. It’s woven into our architecture, licensing, community, and everything else. + +The Open Home is about privacy, choice, and durability. + +## Privacy + +Your home should be your safe space. A place where you can be your true self without having to bother about what the world thinks of you. A place where you don’t need to act differently to avoid an algorithm categorizing your behavior. Privacy for the Open Home means that devices need to work locally. No one else needs to know if you turn on a light bulb or change the thermostat. + +It is okay for a product to offer a cloud connection, but it should be extra and opt-in. + +## Choice + +Devices in your home gather data about themselves and their surroundings. Your data. Vendors shouldn’t be able to limit your access to your data or limit the interoperability of your devices with the rest of your smart home. + +Choice for the Open Home means that devices need to make the gathered data available through local APIs. This avoids vendor lock-in and allows users to create their own smart home with devices from different manufacturers. + +## Durability + +If there is one thing that technology firms are very good at, it is launching new products. However, maintaining the products and making sure they keep working is an afterthought for most. The result is that vendors can decide to no longer support your device, crippling its features or even preventing it from working at all. As we install more and more devices in our home, durability is becoming more and more important. We shouldn’t have to buy everything new every couple of years because the manufacturer decided to move on. + +Durability for the Open Home means that devices are designed and built to keep working. Not just this year, but for the next decade. + +# Our history + +The project was started as a Python application by Paulus Schoutsen in September 2013 and first published publicly on GitHub in November 2013. In July 2017, a managed operating system called Hass.io was initially introduced to make it easier use to use Home Assistant on single-board computers like the Raspberry Pi series. Its bundled "supervisor" management system allowed users to manage, backup, and update the local installation and introduced the option to extend the functionality of the software with add-ons. + +An optional subscription service was introduced in December 2017 for $5/month to solve the complexities associated with secured remote access, as well as linking to Amazon Alexa and Google Assistant. Nabu Casa, Inc. was formed in September 2018 to take over the subscription service. The company's funding is based solely on revenue from the subscription service. It is used to finance the project's infrastructure and to pay for full-time employees contributing to the project. + +In January 2020, branding was adjusted to make it easier to refer to different parts of the project. The main piece of software was renamed to Home Assistant Core, while the full suite of software with the embedded operating system and bundled "supervisor" management system was renamed to Home Assistant. diff --git a/gallery/src/pages/components/ha-alert.markdown b/gallery/src/pages/components/ha-alert.markdown index 2488b83090..e37a205481 100644 --- a/gallery/src/pages/components/ha-alert.markdown +++ b/gallery/src/pages/components/ha-alert.markdown @@ -1,5 +1,6 @@ --- title: Alerts +subtitle: An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task. --- # Alert `` diff --git a/gallery/src/pages/components/ha-faded.ts b/gallery/src/pages/components/ha-faded.ts index 1f962ef101..ad9970fadf 100644 --- a/gallery/src/pages/components/ha-faded.ts +++ b/gallery/src/pages/components/ha-faded.ts @@ -3,18 +3,7 @@ import { customElement } from "lit/decorators"; import "../../../../src/components/ha-card"; import "../../../../src/components/ha-faded"; import "../../../../src/components/ha-markdown"; - -const LONG_TEXT = ` -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum. - -Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci. - -Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo. - -In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla. - -Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim. -`; +import { LONG_TEXT } from "../../data/text"; const SMALL_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index 0722040cc0..4d8ad42b73 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -1,17 +1,109 @@ /* eslint-disable lit/no-template-arrow */ import "@material/mwc-button"; -import { LitElement, TemplateResult, html } from "lit"; +import { html, LitElement, TemplateResult } from "lit"; import { customElement, state } from "lit/decorators"; -import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data"; -import type { HaFormSchema } from "../../../../src/components/ha-form/types"; -import "../../../../src/components/ha-form/ha-form"; -import "../../components/demo-black-white-row"; import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; +import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data"; +import "../../../../src/components/ha-form/ha-form"; +import type { HaFormSchema } from "../../../../src/components/ha-form/types"; +import { getEntity } from "../../../../src/fake_data/entity"; import { provideHass } from "../../../../src/fake_data/provide_hass"; import { HomeAssistant } from "../../../../src/types"; +import "../../components/demo-black-white-row"; + +const ENTITIES = [ + getEntity("alarm_control_panel", "alarm", "disarmed", { + friendly_name: "Alarm", + }), + getEntity("media_player", "livingroom", "playing", { + friendly_name: "Livingroom", + }), + getEntity("media_player", "lounge", "idle", { + friendly_name: "Lounge", + supported_features: 444983, + }), + getEntity("light", "bedroom", "on", { + friendly_name: "Bedroom", + }), + getEntity("switch", "coffee", "off", { + friendly_name: "Coffee", + }), +]; + +const DEVICES = [ + { + area_id: "bedroom", + configuration_url: null, + config_entries: ["config_entry_1"], + connections: [], + disabled_by: null, + entry_type: null, + id: "device_1", + identifiers: [["demo", "volume1"] as [string, string]], + manufacturer: null, + model: null, + name_by_user: null, + name: "Dishwasher", + sw_version: null, + hw_version: null, + via_device_id: null, + }, + { + area_id: "backyard", + configuration_url: null, + config_entries: ["config_entry_2"], + connections: [], + disabled_by: null, + entry_type: null, + id: "device_2", + identifiers: [["demo", "pwm1"] as [string, string]], + manufacturer: null, + model: null, + name_by_user: null, + name: "Lamp", + sw_version: null, + hw_version: null, + via_device_id: null, + }, + { + area_id: null, + configuration_url: null, + config_entries: ["config_entry_3"], + connections: [], + disabled_by: null, + entry_type: null, + id: "device_3", + identifiers: [["demo", "pwm1"] as [string, string]], + manufacturer: null, + model: null, + name_by_user: "User name", + name: "Technical name", + sw_version: null, + hw_version: null, + via_device_id: null, + }, +]; + +const AREAS = [ + { + area_id: "backyard", + name: "Backyard", + picture: null, + }, + { + area_id: "bedroom", + name: "Bedroom", + picture: null, + }, + { + area_id: "livingroom", + name: "Livingroom", + picture: null, + }, +]; const SCHEMAS: { title: string; @@ -39,6 +131,7 @@ const SCHEMAS: { icon: "Icon", media: "Media", location: "Location", + entities: "Entities", }, schema: [ { name: "addon", selector: { addon: {} } }, @@ -46,6 +139,7 @@ const SCHEMAS: { { name: "Attribute", selector: { attribute: { entity_id: "" } }, + context: { filter_entity: "entity" }, }, { name: "Device", selector: { device: {} } }, { name: "Duration", selector: { duration: {} } }, @@ -53,7 +147,9 @@ const SCHEMAS: { { name: "target", selector: { target: {} } }, { name: "number", selector: { number: { min: 0, max: 10 } } }, { name: "boolean", selector: { boolean: {} } }, - { name: "time", selector: { time: {} } }, + { name: "time", required: true, selector: { time: {} } }, + { name: "datetime", required: true, selector: { datetime: {} } }, + { name: "date", required: true, selector: { date: {} } }, { name: "action", selector: { action: {} } }, { name: "text", selector: { text: { multiline: false } } }, { name: "text_multiline", selector: { text: { multiline: true } } }, @@ -80,6 +176,10 @@ const SCHEMAS: { name: "location", selector: { location: { radius: true, icon: "mdi:home" } }, }, + { + name: "entities", + selector: { entity: { multiple: true } }, + }, ], }, { @@ -320,9 +420,10 @@ class DemoHaForm extends LitElement { const hass = provideHass(this); hass.updateTranslations(null, "en"); hass.updateTranslations("config", "en"); + hass.addEntities(ENTITIES); mockEntityRegistry(hass); - mockDeviceRegistry(hass); - mockAreaRegistry(hass); + mockDeviceRegistry(hass, DEVICES); + mockAreaRegistry(hass, AREAS); mockHassioSupervisor(hass); } diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 600176d024..15482b68d0 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -1,20 +1,20 @@ /* eslint-disable lit/no-template-arrow */ import "@material/mwc-button"; -import { LitElement, TemplateResult, css, html } from "lit"; +import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, state } from "lit/decorators"; +import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; +import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; +import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; +import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; import "../../../../src/components/ha-selector/ha-selector"; import "../../../../src/components/ha-settings-row"; +import { BlueprintInput } from "../../../../src/data/blueprint"; +import { showDialog } from "../../../../src/dialogs/make-dialog-manager"; +import { getEntity } from "../../../../src/fake_data/entity"; import { provideHass } from "../../../../src/fake_data/provide_hass"; +import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin"; import type { HomeAssistant } from "../../../../src/types"; import "../../components/demo-black-white-row"; -import { BlueprintInput } from "../../../../src/data/blueprint"; -import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; -import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; -import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; -import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; -import { getEntity } from "../../../../src/fake_data/entity"; -import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin"; -import { showDialog } from "../../../../src/dialogs/make-dialog-manager"; const ENTITIES = [ getEntity("alarm_control_panel", "alarm", "disarmed", { @@ -109,7 +109,7 @@ const AREAS = [ const SCHEMAS: { name: string; - input: Record; + input: Record; }[] = [ { name: "One of each", @@ -146,6 +146,8 @@ const SCHEMAS: { }, boolean: { name: "Boolean", selector: { boolean: {} } }, time: { name: "Time", selector: { time: {} } }, + date: { name: "Date", selector: { date: {} } }, + datetime: { name: "Date Time", selector: { datetime: {} } }, action: { name: "Action", selector: { action: {} } }, text: { name: "Text", @@ -162,9 +164,42 @@ const SCHEMAS: { }, }, object: { name: "Object", selector: { object: {} } }, + select_radio: { + name: "Select (Radio)", + selector: { + select: { options: ["Option 1", "Option 2"], mode: "list" }, + }, + }, select: { name: "Select", - selector: { select: { options: ["Option 1", "Option 2"] } }, + selector: { + select: { + options: [ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + ], + }, + }, + }, + select_custom: { + name: "Select (Custom)", + selector: { + select: { + custom_value: true, + options: [ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + ], + }, + }, }, icon: { name: "Icon", selector: { icon: {} } }, media: { name: "Media", selector: { media: {} } }, @@ -173,6 +208,47 @@ const SCHEMAS: { name: "Location with radius", selector: { location: { radius: true, icon: "mdi:home" } }, }, + color_temp: { + name: "Color Temperature", + selector: { color_temp: {} }, + }, + color_rgb: { name: "Color", selector: { color_rgb: {} } }, + }, + }, + { + name: "Multiples", + input: { + entity: { name: "Entity", selector: { entity: { multiple: true } } }, + device: { name: "Device", selector: { device: { multiple: true } } }, + area: { name: "Area", selector: { area: { multiple: true } } }, + select: { + name: "Select Multiple", + selector: { + select: { + multiple: true, + custom_value: true, + options: [ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + ], + }, + }, + }, + select_checkbox: { + name: "Select Multiple (Checkbox)", + required: false, + selector: { + select: { + mode: "list", + multiple: true, + options: ["Option 1", "Option 2", "Option 3", "Option 4"], + }, + }, + }, }, }, ]; @@ -181,6 +257,12 @@ const SCHEMAS: { class DemoHaSelector extends LitElement implements ProvideHassElement { @state() public hass!: HomeAssistant; + @state() private _disabled = false; + + @state() private _required = false; + + @state() private _label = true; + private data = SCHEMAS.map(() => ({})); constructor() { @@ -314,6 +396,29 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { protected render(): TemplateResult { return html` +
+ + + + + + + + + +
${SCHEMAS.map((info, idx) => { const data = this.data[idx]; const valueChanged = (ev) => { @@ -336,7 +441,10 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { .hass=${this.hass} .selector=${value!.selector} .key=${key} + .label=${this._label ? value!.name : undefined} .value=${data[key] ?? value!.default} + .disabled=${this._disabled} + .required=${this._required} @value-changed=${valueChanged} > @@ -349,10 +457,20 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { `; } + private _handleOptionChange(ev) { + this[`_${ev.target.name}`] = ev.target.checked; + } + static styles = css` ha-selector { width: 60; } + .options { + padding: 16px 48px; + } + .options ha-formfield { + margin-right: 16px; + } `; } diff --git a/gallery/src/pages/design.home-assistant.io/editing.markdown b/gallery/src/pages/design.home-assistant.io/editing.markdown index 2a503ced6a..742a79eeea 100644 --- a/gallery/src/pages/design.home-assistant.io/editing.markdown +++ b/gallery/src/pages/design.home-assistant.io/editing.markdown @@ -2,6 +2,8 @@ title: Editing design.home-assistant.io --- +![Home Assistant Logo](/images/logo-with-text.png) + # How to edit design.home-assistant.io All pages are stored in [the pages folder][pages-folder] on GitHub. Pages are grouped in a folder per sidebar section. Each page can contain a `.markdown` description file, a `.ts` demo file or both. If both are defined the description is rendered first. The description can contain metadata to specify the title of the page. @@ -41,15 +43,12 @@ import { html, css, LitElement } from "lit"; import { customElement } from "lit/decorators"; import "../../../../src/components/ha-card"; - @customElement("demo-user-experience-usability") export class DemoUserExperienceUsability extends LitElement { protected render() { return html` -
- Hello world! -
+
Hello world!
`; } diff --git a/gallery/src/pages/lovelace/markdown-card.ts b/gallery/src/pages/lovelace/markdown-card.ts index 2b6257cd23..1abca4bdcc 100644 --- a/gallery/src/pages/lovelace/markdown-card.ts +++ b/gallery/src/pages/lovelace/markdown-card.ts @@ -9,7 +9,7 @@ const CONFIGS = [ heading: "markdown-it demo", config: ` - type: markdown - content: > + content: >- # h1 Heading 8-) ## h2 Heading @@ -249,6 +249,17 @@ const CONFIGS = [ ::: warning *here be dragons* ::: + + ### ha-alert + + You can use our [\`ha-alert\`](https://design.home-assistant.io/#components/ha-alert) component in markdown content rendered in the Home Assistant Frontend. + + This is an error alert — check it out! + This is a warning alert — check it out! + This is an info alert — check it out! + This is a success alert — check it out! + This is an alert with a title + `, }, ]; diff --git a/gallery/src/pages/misc/integration-card.ts b/gallery/src/pages/misc/integration-card.ts index 53f16a16b2..aa26eb2c6b 100644 --- a/gallery/src/pages/misc/integration-card.ts +++ b/gallery/src/pages/misc/integration-card.ts @@ -188,6 +188,7 @@ const createEntityRegistryEntries = ( device_id: "mock-device-id", area_id: null, disabled_by: null, + hidden_by: null, entity_category: null, entity_id: "binary_sensor.updater", name: null, diff --git a/gallery/src/pages/more-info/update.markdown b/gallery/src/pages/more-info/update.markdown new file mode 100644 index 0000000000..e7540412e3 --- /dev/null +++ b/gallery/src/pages/more-info/update.markdown @@ -0,0 +1,3 @@ +--- +title: Update +--- diff --git a/gallery/src/pages/more-info/update.ts b/gallery/src/pages/more-info/update.ts new file mode 100644 index 0000000000..f23ae25780 --- /dev/null +++ b/gallery/src/pages/more-info/update.ts @@ -0,0 +1,178 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import { + UPDATE_SUPPORT_BACKUP, + UPDATE_SUPPORT_PROGRESS, + UPDATE_SUPPORT_INSTALL, + UPDATE_SUPPORT_RELEASE_NOTES, +} from "../../../../src/data/update"; +import "../../../../src/dialogs/more-info/more-info-content"; +import { getEntity } from "../../../../src/fake_data/entity"; +import { + MockHomeAssistant, + provideHass, +} from "../../../../src/fake_data/provide_hass"; +import "../../components/demo-more-infos"; +import { LONG_TEXT } from "../../data/text"; + +const base_attributes = { + title: "Awesome", + current_version: "1.2.2", + latest_version: "1.2.3", + release_url: "https://home-assistant.io", + supported_features: UPDATE_SUPPORT_INSTALL, + skipped_version: null, + in_progress: false, + release_summary: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In nec metus aliquet, porta mi ut, ultrices odio. Etiam egestas orci tellus, non semper metus blandit tincidunt. Praesent elementum turpis vel tempor pharetra. Sed quis cursus diam. Proin sem justo.", +}; + +const ENTITIES = [ + getEntity("update", "update1", "on", { + ...base_attributes, + friendly_name: "Update", + }), + getEntity("update", "update2", "on", { + ...base_attributes, + title: null, + friendly_name: "Update without title", + }), + getEntity("update", "update3", "on", { + ...base_attributes, + release_url: null, + friendly_name: "Update without release_url", + }), + getEntity("update", "update4", "on", { + ...base_attributes, + release_summary: null, + friendly_name: "Update without release_summary", + }), + getEntity("update", "update5", "off", { + ...base_attributes, + current_version: "1.2.3", + friendly_name: "No update", + }), + getEntity("update", "update6", "off", { + ...base_attributes, + skipped_version: "1.2.3", + friendly_name: "Skipped version", + }), + getEntity("update", "update7", "on", { + ...base_attributes, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_BACKUP, + friendly_name: "With backup support", + }), + getEntity("update", "update8", "on", { + ...base_attributes, + in_progress: true, + friendly_name: "With true in_progress", + }), + getEntity("update", "update9", "on", { + ...base_attributes, + in_progress: 25, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, + friendly_name: "With 25 in_progress", + }), + getEntity("update", "update10", "on", { + ...base_attributes, + in_progress: 50, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, + friendly_name: "With 50 in_progress", + }), + getEntity("update", "update11", "on", { + ...base_attributes, + in_progress: 75, + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, + friendly_name: "With 75 in_progress", + }), + getEntity("update", "update12", "unavailable", { + ...base_attributes, + in_progress: 50, + friendly_name: "Unavailable", + }), + getEntity("update", "update13", "on", { + ...base_attributes, + supported_features: 0, + friendly_name: "No install support", + }), + getEntity("update", "update14", "off", { + ...base_attributes, + current_version: null, + friendly_name: "Update without current_version", + }), + getEntity("update", "update15", "off", { + ...base_attributes, + latest_version: null, + friendly_name: "Update without latest_version", + }), + getEntity("update", "update16", "off", { + ...base_attributes, + friendly_name: "Update with release notes", + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES, + }), + getEntity("update", "update17", "off", { + ...base_attributes, + friendly_name: "Update with release notes error", + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES, + }), + getEntity("update", "update18", "off", { + ...base_attributes, + friendly_name: "Update with release notes loading", + supported_features: + base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES, + }), +]; + +@customElement("demo-more-info-update") +class DemoMoreInfoUpdate extends LitElement { + @property() public hass!: MockHomeAssistant; + + @query("demo-more-infos") private _demoRoot!: HTMLElement; + + protected render(): TemplateResult { + return html` + ent.entityId)} + > + `; + } + + protected firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + const hass = provideHass(this._demoRoot); + hass.updateTranslations(null, "en"); + hass.addEntities(ENTITIES); + hass.mockWS( + "update/release_notes", + (msg: { type: string; entity_id: string }) => { + if (msg.entity_id === "update.update16") { + return LONG_TEXT; + } + if (msg.entity_id === "update.update17") { + return Promise.reject({ + code: "error", + message: "Could not fetch release notes", + }); + } + if (msg.entity_id === "update.update18") { + return undefined; + } + return null; + } + ); + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-more-info-update": DemoMoreInfoUpdate; + } +} diff --git a/gallery/src/pages/user-test/user-types.markdown b/gallery/src/pages/user-test/user-types.markdown new file mode 100644 index 0000000000..eacc108cd4 --- /dev/null +++ b/gallery/src/pages/user-test/user-types.markdown @@ -0,0 +1,17 @@ +--- +title: "User types" +--- + +We have defined three user types for Home Assistant. They are a lean segmentation of users that helps us make decisions throughout the product. User types differ from traditional personas in that the segmentation criteria aren’t demographic and don’t personify a group into a single character with a fictitious background story. + +# Outgrowers + +Users that outgrow big tech smart home solutions. It just needs to work with easy setup via an app. + +# Tinkerers + +Technoid users in home networking and development that know how to code. + +# Questioner + +Users who want more advanced home automation, but need support to make it work. diff --git a/hassio/src/entrypoint.ts b/hassio/src/entrypoint.ts index 48c4a33180..09e73ecf4d 100644 --- a/hassio/src/entrypoint.ts +++ b/hassio/src/entrypoint.ts @@ -1,9 +1,12 @@ // Compat needs to be first import import "../../src/resources/compatibility"; +import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings"; import "../../src/resources/roboto"; import "../../src/resources/safari-14-attachshadow-patch"; import "./hassio-main"; +setCancelSyntheticClickEvents(false); + const styleEl = document.createElement("style"); styleEl.innerHTML = ` body { diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index a5a2bc81a2..27c3533b8e 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -45,7 +45,6 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-tabs-subpage"; -import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates"; import { HomeAssistant, Route } from "../../../src/types"; import { addonArchIsSupported, extractChangelog } from "../util/addon"; @@ -55,6 +54,12 @@ declare global { } } +const SUPERVISOR_UPDATE_NAMES = { + core: "Home Assistant Core", + os: "Home Assistant Operating System", + supervisor: "Home Assistant Supervisor", +}; + type updateType = "os" | "supervisor" | "core" | "addon"; const changelogUrl = ( diff --git a/package.json b/package.json index c9e04cedd0..304c43e2aa 100644 --- a/package.json +++ b/package.json @@ -72,14 +72,13 @@ "@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.5.95", - "@mdi/svg": "6.5.95", + "@mdi/js": "6.6.95", + "@mdi/svg": "6.6.95", "@polymer/app-layout": "^3.1.0", "@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-icon": "^3.0.1", "@polymer/iron-input": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1", - "@polymer/paper-dropdown-menu": "^3.2.0", "@polymer/paper-input": "^3.2.1", "@polymer/paper-item": "^3.0.1", "@polymer/paper-listbox": "^3.0.1", @@ -109,7 +108,7 @@ "fuse.js": "^6.0.0", "google-timezones-json": "^1.0.2", "hls.js": "^1.1.5", - "home-assistant-js-websocket": "^6.0.1", + "home-assistant-js-websocket": "^7.0.1", "idb-keyval": "^5.1.3", "intl-messageformat": "^9.9.1", "js-yaml": "^4.1.0", @@ -136,7 +135,6 @@ "vis-network": "^8.5.4", "vue": "^2.6.12", "vue2-daterange-picker": "^0.5.1", - "web-animations-js": "^2.3.2", "workbox-cacheable-response": "^6.4.2", "workbox-core": "^6.4.2", "workbox-expiration": "^6.4.2", diff --git a/setup.cfg b/setup.cfg index a73c6937e0..eee0210ac8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = home-assistant-frontend -version = 20220301.2 +version = 20220330.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 69bf65dd8a..0000000000 --- a/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Entry point for setuptools. Required for editable installs. -TODO: Remove file after updating to pip 21.3 -""" -from setuptools import setup - -setup() diff --git a/src/common/const.ts b/src/common/const.ts index f11e7d7380..ee9f94000c 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -187,6 +187,7 @@ export const DOMAINS_WITH_MORE_INFO = [ "scene", "sun", "timer", + "update", "vacuum", "water_heater", "weather", @@ -200,6 +201,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [ "input_text", "number", "scene", + "update", "select", ]; diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 3838218ea4..e78b889457 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -1,12 +1,18 @@ import { HassEntity } from "home-assistant-js-websocket"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { FrontendLocaleData } from "../../data/translation"; +import { + updateIsInstalling, + UpdateEntity, + UPDATE_SUPPORT_PROGRESS, +} from "../../data/update"; import { formatDate } from "../datetime/format_date"; import { formatDateTime } from "../datetime/format_date_time"; import { formatTime } from "../datetime/format_time"; import { formatNumber, isNumericState } from "../number/format_number"; import { LocalizeFunc } from "../translations/localize"; import { computeStateDomain } from "./compute_state_domain"; +import { supportsFeature } from "./supports-feature"; export const computeStateDisplay = ( localize: LocalizeFunc, @@ -130,6 +136,28 @@ export const computeStateDisplay = ( } } + if (domain === "update") { + // When updating, and entity does not support % show "Installing" + // When updating, and entity does support % show "Installing (xx%)" + // When update available, show the version + // When the latest version is skipped, show the latest version + // When update is not available, show "Up-to-date" + // When update is not available and there is no latest_version show "Unavailable" + return compareState === "on" + ? updateIsInstalling(stateObj as UpdateEntity) + ? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS) + ? localize("ui.card.update.installing_with_progress", { + progress: stateObj.attributes.in_progress, + }) + : localize("ui.card.update.installing") + : stateObj.attributes.latest_version + : stateObj.attributes.skipped_version === + stateObj.attributes.latest_version + ? stateObj.attributes.latest_version ?? + localize("state.default.unavailable") + : localize("ui.card.update.up_to_date"); + } + return ( // Return device class translation (stateObj.attributes.device_class && diff --git a/src/common/entity/compute_state_domain.ts b/src/common/entity/compute_state_domain.ts index b4408257a6..1b972ea22f 100644 --- a/src/common/entity/compute_state_domain.ts +++ b/src/common/entity/compute_state_domain.ts @@ -1,4 +1,4 @@ -import { HassEntity } from "home-assistant-js-websocket"; +import type { HassEntity } from "home-assistant-js-websocket"; import { computeDomain } from "./compute_domain"; export const computeStateDomain = (stateObj: HassEntity) => diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index af56f1135b..af89f00d16 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -9,11 +9,10 @@ import { mdiCast, mdiCastConnected, mdiClock, - mdiEmoticonDead, - mdiFlash, mdiGestureTapButton, mdiLanConnect, mdiLanDisconnect, + mdiLightSwitch, mdiLock, mdiLockAlert, mdiLockClock, @@ -22,16 +21,16 @@ import { mdiPowerPlug, mdiPowerPlugOff, mdiRestart, - mdiSleep, - mdiTimerSand, mdiToggleSwitch, mdiToggleSwitchOff, mdiCheckCircleOutline, mdiCloseCircleOutline, mdiWeatherNight, - mdiZWave, + mdiPackage, + mdiPackageDown, } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; +import { updateIsInstalling, UpdateEntity } from "../../data/update"; /** * Return the icon to be used for a domain. * @@ -112,19 +111,7 @@ export const domainIcon = ( case "switch": return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff; default: - return mdiFlash; - } - - case "zwave": - switch (compareState) { - case "dead": - return mdiEmoticonDead; - case "sleeping": - return mdiSleep; - case "initializing": - return mdiTimerSand; - default: - return mdiZWave; + return mdiLightSwitch; } case "sensor": { @@ -149,6 +136,13 @@ export const domainIcon = ( return stateObj?.state === "above_horizon" ? FIXED_DOMAIN_ICONS[domain] : mdiWeatherNight; + + case "update": + return compareState === "on" + ? updateIsInstalling(stateObj as UpdateEntity) + ? mdiPackageDown + : mdiPackageUp + : mdiPackage; } if (domain in FIXED_DOMAIN_ICONS) { diff --git a/src/common/string/is_ip_address.ts b/src/common/string/is_ip_address.ts new file mode 100644 index 0000000000..8f0277176a --- /dev/null +++ b/src/common/string/is_ip_address.ts @@ -0,0 +1,4 @@ +const regexp = + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + +export const isIPAddress = (input: string): boolean => regexp.test(input); diff --git a/src/common/style/icon_color_css.ts b/src/common/style/icon_color_css.ts index 7e00bf1c77..3f4faeed99 100644 --- a/src/common/style/icon_color_css.ts +++ b/src/common/style/icon_color_css.ts @@ -7,6 +7,7 @@ export const iconColorCSS = css` ha-state-icon[data-domain="calendar"][data-state="on"], ha-state-icon[data-domain="camera"][data-state="streaming"], ha-state-icon[data-domain="cover"][data-state="open"], + ha-state-icon[data-domain="device_tracker"][data-state="home"], ha-state-icon[data-domain="fan"][data-state="on"], ha-state-icon[data-domain="humidifier"][data-state="on"], ha-state-icon[data-domain="light"][data-state="on"], @@ -70,9 +71,6 @@ export const iconColorCSS = css` } ha-state-icon[data-domain="plant"][data-state="problem"], - ha-state-icon[data-domain="zwave"][data-state="dead"] { - color: var(--state-icon-error-color); - } /* Color the icon if unavailable */ ha-state-icon[data-state="unavailable"] { diff --git a/src/common/util/time-cache-entity-promise-func.ts b/src/common/util/time-cache-entity-promise-func.ts new file mode 100644 index 0000000000..0b3cc5a293 --- /dev/null +++ b/src/common/util/time-cache-entity-promise-func.ts @@ -0,0 +1,53 @@ +import { HomeAssistant } from "../../types"; + +interface ResultCache { + [entityId: string]: Promise | undefined; +} + +/** + * Call a function with result caching per entity. + * @param cacheKey key to store the cache on hass object + * @param cacheTime time to cache the results + * @param func function to fetch the data + * @param hass Home Assistant object + * @param entityId entity to fetch data for + * @param args extra arguments to pass to the function to fetch the data + * @returns + */ +export const timeCacheEntityPromiseFunc = async ( + cacheKey: string, + cacheTime: number, + func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise, + hass: HomeAssistant, + entityId: string, + ...args: any[] +): Promise => { + let cache: ResultCache | undefined = (hass as any)[cacheKey]; + + if (!cache) { + cache = hass[cacheKey] = {}; + } + + const lastResult = cache[entityId]; + + if (lastResult) { + return lastResult; + } + + const result = func(hass, entityId, ...args); + cache[entityId] = result; + + result.then( + // When successful, set timer to clear cache + () => + setTimeout(() => { + cache![entityId] = undefined; + }, cacheTime), + // On failure, clear cache right away + () => { + cache![entityId] = undefined; + } + ); + + return result; +}; diff --git a/src/common/util/time-cache-function-promise.ts b/src/common/util/time-cache-function-promise.ts index d841ea256b..daa4b730b5 100644 --- a/src/common/util/time-cache-function-promise.ts +++ b/src/common/util/time-cache-function-promise.ts @@ -1,43 +1,80 @@ import { HomeAssistant } from "../../types"; -interface ResultCache { - [entityId: string]: Promise | undefined; +interface CacheResult { + result: T; + cacheKey: any; } +/** + * Caches a result of a promise for X time. Allows optional extra validation + * check to invalidate the cache. + * @param cacheKey the key to store the cache + * @param cacheTime the time to cache the result + * @param func the function to fetch the data + * @param generateCacheKey optional function to generate a cache key based on current hass + cached result. Cache is invalid if generates a different cache key. + * @param hass Home Assistant object + * @param args extra arguments to pass to the function to fetch the data + * @returns + */ export const timeCachePromiseFunc = async ( cacheKey: string, cacheTime: number, - func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise, + func: (hass: HomeAssistant, ...args: any[]) => Promise, + generateCacheKey: + | ((hass: HomeAssistant, lastResult: T) => unknown) + | undefined, hass: HomeAssistant, - entityId: string, ...args: any[] ): Promise => { - let cache: ResultCache | undefined = (hass as any)[cacheKey]; + const anyHass = hass as any; + const lastResult: Promise> | CacheResult | undefined = + anyHass[cacheKey]; - if (!cache) { - cache = hass[cacheKey] = {}; - } + const checkCachedResult = (result: CacheResult): T | Promise => { + if ( + !generateCacheKey || + generateCacheKey(hass, result.result) === result.cacheKey + ) { + return result.result; + } - const lastResult = cache[entityId]; + anyHass[cacheKey] = undefined; + return timeCachePromiseFunc( + cacheKey, + cacheTime, + func, + generateCacheKey, + hass, + ...args + ); + }; + // If we have a cached result, return it if it's still valid if (lastResult) { - return lastResult; + return lastResult instanceof Promise + ? lastResult.then(checkCachedResult) + : checkCachedResult(lastResult); } - const result = func(hass, entityId, ...args); - cache[entityId] = result; + const resultPromise = func(hass, ...args); + anyHass[cacheKey] = resultPromise; - result.then( + resultPromise.then( // When successful, set timer to clear cache - () => + (result) => { + anyHass[cacheKey] = { + result, + cacheKey: generateCacheKey?.(hass, result), + }; setTimeout(() => { - cache![entityId] = undefined; - }, cacheTime), + anyHass[cacheKey] = undefined; + }, cacheTime); + }, // On failure, clear cache right away () => { - cache![entityId] = undefined; + anyHass[cacheKey] = undefined; } ); - return result; + return resultPromise; }; diff --git a/src/components/buttons/ha-progress-button.ts b/src/components/buttons/ha-progress-button.ts index 3272e8be1d..d025cb7ff3 100644 --- a/src/components/buttons/ha-progress-button.ts +++ b/src/components/buttons/ha-progress-button.ts @@ -1,8 +1,9 @@ import "@material/mwc-button"; -import type { Button } from "@material/mwc-button"; +import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import "../ha-circular-progress"; +import "../ha-svg-icon"; @customElement("ha-progress-button") export class HaProgressButton extends LitElement { @@ -12,38 +13,53 @@ export class HaProgressButton extends LitElement { @property({ type: Boolean }) public raised = false; - @query("mwc-button", true) private _button?: Button; + @state() private _result?: "success" | "error"; public render(): TemplateResult { + const overlay = this._result || this.progress; return html` - ${this.progress - ? html`
- -
` - : ""} + ${!overlay + ? "" + : html` +
+ ${this._result === "success" + ? html`` + : this._result === "error" + ? html`` + : this.progress + ? html` + + ` + : ""} +
+ `} `; } public actionSuccess(): void { - this._tempClass("success"); + this._setResult("success"); } public actionError(): void { - this._tempClass("error"); + this._setResult("error"); } - private _tempClass(className: string): void { - this._button!.classList.add(className); + private _setResult(result: "success" | "error"): void { + this._result = result; setTimeout(() => { - this._button!.classList.remove(className); - }, 1000); + this._result = undefined; + }, 2000); } private _buttonTapped(ev: Event): void { @@ -69,6 +85,7 @@ export class HaProgressButton extends LitElement { background-color: var(--success-color); transition: none; border-radius: 4px; + pointer-events: none; } mwc-button[raised].success { @@ -81,6 +98,7 @@ export class HaProgressButton extends LitElement { background-color: var(--error-color); transition: none; border-radius: 4px; + pointer-events: none; } mwc-button[raised].error { @@ -89,13 +107,21 @@ export class HaProgressButton extends LitElement { } .progress { - bottom: 0; - margin-top: 4px; + bottom: 4px; position: absolute; text-align: center; - top: 0; + top: 4px; width: 100%; } + + ha-svg-icon { + color: white; + } + + mwc-button.success slot, + mwc-button.error slot { + visibility: hidden; + } `; } } diff --git a/src/components/date-range-picker.ts b/src/components/date-range-picker.ts index f0c0951a77..e6a5c6479f 100644 --- a/src/components/date-range-picker.ts +++ b/src/components/date-range-picker.ts @@ -115,7 +115,7 @@ class DateRangePickerElement extends WrappedElement { color: var(--primary-text-color); min-width: initial !important; } - .daterangepicker:after { + .daterangepicker:before { display: none; } .daterangepicker:after { diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index d1a7d2b55a..ea62482637 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -86,6 +86,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public disabled?: boolean; + @property({ type: Boolean }) public required?: boolean; + @state() private _opened?: boolean; @query("ha-combo-box", true) public comboBox!: HaComboBox; @@ -269,6 +271,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { .value=${this._value} .renderer=${rowRenderer} .disabled=${this.disabled} + .required=${this.required} item-value-path="id" item-label-path="name" @opened-changed=${this._openedChanged} diff --git a/src/components/device/ha-devices-picker.ts b/src/components/device/ha-devices-picker.ts index 1b437da780..9ca65e20a5 100644 --- a/src/components/device/ha-devices-picker.ts +++ b/src/components/device/ha-devices-picker.ts @@ -1,4 +1,4 @@ -import { html, LitElement, TemplateResult } from "lit"; +import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { PolymerChangedEvent } from "../../polymer-types"; @@ -11,6 +11,8 @@ class HaDevicesPicker extends LitElement { @property() public value?: string[]; + @property({ type: Boolean }) public required?: boolean; + /** * Show entities from specific domains. * @type {string} @@ -66,6 +68,7 @@ class HaDevicesPicker extends LitElement { .excludeDomains=${this.excludeDomains} .includeDeviceClasses=${this.includeDeviceClasses} .label=${this.pickDeviceLabel} + .required=${this.required} @value-changed=${this._addDevice} > @@ -116,6 +119,12 @@ class HaDevicesPicker extends LitElement { this._updateDevices([...currentDevices, toAdd]); } + + static override styles = css` + div { + margin-top: 8px; + } + `; } declare global { diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts index 35e21dc950..75002c51d4 100644 --- a/src/components/entity/ha-entities-picker.ts +++ b/src/components/entity/ha-entities-picker.ts @@ -14,6 +14,8 @@ class HaEntitiesPickerLight extends LitElement { @property({ type: Array }) public value?: string[]; + @property({ type: Boolean }) public required?: boolean; + /** * Show entities from specific domains. * @type {string} @@ -46,11 +48,29 @@ class HaEntitiesPickerLight extends LitElement { @property({ type: Array, attribute: "include-unit-of-measurement" }) public includeUnitOfMeasurement?: string[]; + /** + * List of allowed entities to show. Will ignore all other filters. + * @type {Array} + * @attr include-entities + */ + @property({ type: Array, attribute: "include-entities" }) + public includeEntities?: string[]; + + /** + * List of entities to be excluded. + * @type {Array} + * @attr exclude-entities + */ + @property({ type: Array, attribute: "exclude-entities" }) + public excludeEntities?: string[]; + @property({ attribute: "picked-entity-label" }) public pickedEntityLabel?: string; @property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string; + @property() public entityFilter?: HaEntityPickerEntityFilterFunc; + protected render(): TemplateResult { if (!this.hass) { return html``; @@ -67,6 +87,8 @@ class HaEntitiesPickerLight extends LitElement { .hass=${this.hass} .includeDomains=${this.includeDomains} .excludeDomains=${this.excludeDomains} + .includeEntities=${this.includeEntities} + .excludeEntities=${this.excludeEntities} .includeDeviceClasses=${this.includeDeviceClasses} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .entityFilter=${this._entityFilter} @@ -82,10 +104,13 @@ class HaEntitiesPickerLight extends LitElement { .hass=${this.hass} .includeDomains=${this.includeDomains} .excludeDomains=${this.excludeDomains} + .includeEntities=${this.includeEntities} + .excludeEntities=${this.excludeEntities} .includeDeviceClasses=${this.includeDeviceClasses} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .entityFilter=${this._entityFilter} .label=${this.pickEntityLabel} + .required=${this.required} @value-changed=${this._addEntity} > @@ -94,7 +119,9 @@ class HaEntitiesPickerLight extends LitElement { private _entityFilter: HaEntityPickerEntityFilterFunc = ( stateObj: HassEntity - ) => !this.value || !this.value.includes(stateObj.entity_id); + ) => + (!this.value || !this.value.includes(stateObj.entity_id)) && + (!this.entityFilter || this.entityFilter(stateObj)); private get _currentEntities() { return this.value || []; diff --git a/src/components/entity/ha-entity-attribute-picker.ts b/src/components/entity/ha-entity-attribute-picker.ts index 291d30da5e..49bfd1db33 100644 --- a/src/components/entity/ha-entity-attribute-picker.ts +++ b/src/components/entity/ha-entity-attribute-picker.ts @@ -19,6 +19,8 @@ class HaEntityAttributePicker extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = false; + @property({ type: Boolean, attribute: "allow-custom-value" }) public allowCustomValue; @@ -61,6 +63,7 @@ class HaEntityAttributePicker extends LitElement { "ui.components.entity.entity-attribute-picker.attribute" )} .disabled=${this.disabled || !this.entityId} + .required=${this.required} .allowCustomValue=${this.allowCustomValue} item-value-path="value" item-label-path="label" diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index d24f6fbac4..c8f288ad70 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -7,6 +7,7 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; +import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import "../ha-combo-box"; @@ -15,18 +16,21 @@ import "../ha-icon-button"; import "../ha-svg-icon"; import "./state-badge"; +interface HassEntityWithCachedName extends HassEntity { + friendly_name: string; +} + export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; // eslint-disable-next-line lit/prefer-static-styles -const rowRenderer: ComboBoxLitRenderer = - (item) => - html` - ${item.state - ? html`` - : ""} - ${item.friendly_name} - ${item.entity_id} - `; +const rowRenderer: ComboBoxLitRenderer = (item) => + html` + ${item.state + ? html`` + : ""} + ${item.friendly_name} + ${item.entity_id} + `; @customElement("ha-entity-picker") export class HaEntityPicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -35,6 +39,8 @@ export class HaEntityPicker extends LitElement { @property({ type: Boolean }) public disabled?: boolean; + @property({ type: Boolean }) public required?: boolean; + @property({ type: Boolean, attribute: "allow-custom-entity" }) public allowCustomEntity; @@ -74,6 +80,22 @@ export class HaEntityPicker extends LitElement { @property({ type: Array, attribute: "include-unit-of-measurement" }) public includeUnitOfMeasurement?: string[]; + /** + * List of allowed entities to show. Will ignore all other filters. + * @type {Array} + * @attr include-entities + */ + @property({ type: Array, attribute: "include-entities" }) + public includeEntities?: string[]; + + /** + * List of entities to be excluded. + * @type {Array} + * @attr exclude-entities + */ + @property({ type: Array, attribute: "exclude-entities" }) + public excludeEntities?: string[]; + @property() public entityFilter?: HaEntityPickerEntityFilterFunc; @property({ type: Boolean }) public hideClearIcon = false; @@ -96,7 +118,7 @@ export class HaEntityPicker extends LitElement { private _initedStates = false; - private _states: HassEntity[] = []; + private _states: HassEntityWithCachedName[] = []; private _getStates = memoizeOne( ( @@ -106,9 +128,11 @@ export class HaEntityPicker extends LitElement { excludeDomains: this["excludeDomains"], entityFilter: this["entityFilter"], includeDeviceClasses: this["includeDeviceClasses"], - includeUnitOfMeasurement: this["includeUnitOfMeasurement"] - ) => { - let states: HassEntity[] = []; + includeUnitOfMeasurement: this["includeUnitOfMeasurement"], + includeEntities: this["includeEntities"], + excludeEntities: this["excludeEntities"] + ): HassEntityWithCachedName[] => { + let states: HassEntityWithCachedName[] = []; if (!hass) { return []; @@ -122,7 +146,7 @@ export class HaEntityPicker extends LitElement { state: "", last_changed: "", last_updated: "", - context: { id: "", user_id: null }, + context: { id: "", user_id: null, parent_id: null }, friendly_name: this.hass!.localize( "ui.components.entity.entity-picker.no_entities" ), @@ -136,6 +160,30 @@ export class HaEntityPicker extends LitElement { ]; } + if (includeEntities) { + entityIds = entityIds.filter((entityId) => + this.includeEntities!.includes(entityId) + ); + + return entityIds + .map((key) => ({ + ...hass!.states[key], + friendly_name: computeStateName(hass!.states[key]) || key, + })) + .sort((entityA, entityB) => + caseInsensitiveStringCompare( + entityA.friendly_name, + entityB.friendly_name + ) + ); + } + + if (excludeEntities) { + entityIds = entityIds.filter( + (entityId) => !excludeEntities!.includes(entityId) + ); + } + if (includeDomains) { entityIds = entityIds.filter((eid) => includeDomains.includes(computeDomain(eid)) @@ -148,10 +196,17 @@ export class HaEntityPicker extends LitElement { ); } - states = entityIds.sort().map((key) => ({ - ...hass!.states[key], - friendly_name: computeStateName(hass!.states[key]) || key, - })); + states = entityIds + .map((key) => ({ + ...hass!.states[key], + friendly_name: computeStateName(hass!.states[key]) || key, + })) + .sort((entityA, entityB) => + caseInsensitiveStringCompare( + entityA.friendly_name, + entityB.friendly_name + ) + ); if (includeDeviceClasses) { states = states.filter( @@ -190,7 +245,7 @@ export class HaEntityPicker extends LitElement { state: "", last_changed: "", last_updated: "", - context: { id: "", user_id: null }, + context: { id: "", user_id: null, parent_id: null }, friendly_name: this.hass!.localize( "ui.components.entity.entity-picker.no_match" ), @@ -228,7 +283,9 @@ export class HaEntityPicker extends LitElement { this.excludeDomains, this.entityFilter, this.includeDeviceClasses, - this.includeUnitOfMeasurement + this.includeUnitOfMeasurement, + this.includeEntities, + this.excludeEntities ); if (this._initedStates) { (this.comboBox as any).filteredItems = this._states; @@ -250,6 +307,7 @@ export class HaEntityPicker extends LitElement { .allowCustomValue=${this.allowCustomEntity} .filteredItems=${this._states} .renderer=${rowRenderer} + .required=${this.required} @opened-changed=${this._openedChanged} @value-changed=${this._valueChanged} @filter-changed=${this._filterChanged} diff --git a/src/components/ha-addon-picker.ts b/src/components/ha-addon-picker.ts index a28f188de9..1dd6a11cea 100644 --- a/src/components/ha-addon-picker.ts +++ b/src/components/ha-addon-picker.ts @@ -30,6 +30,8 @@ class HaAddonPicker extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = false; + @query("ha-combo-box") private _comboBox!: HaComboBox; public open() { @@ -55,6 +57,8 @@ class HaAddonPicker extends LitElement { ? this.hass.localize("ui.components.addon-picker.addon") : this.label} .value=${this._value} + .required=${this.required} + .disabled=${this.disabled} .renderer=${rowRenderer} .items=${this._addons} item-value-path="slug" diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index a671893737..b841575b67 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -28,8 +28,8 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { PolymerChangedEvent } from "../polymer-types"; import { HomeAssistant } from "../types"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; -import type { HaComboBox } from "./ha-combo-box"; import "./ha-combo-box"; +import type { HaComboBox } from "./ha-combo-box"; import "./ha-icon-button"; import "./ha-svg-icon"; @@ -84,6 +84,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public disabled?: boolean; + @property({ type: Boolean }) public required?: boolean; + @state() private _areas?: AreaRegistryEntry[]; @state() private _devices?: DeviceRegistryEntry[]; @@ -315,6 +317,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { item-label-path="name" .value=${this.value} .disabled=${this.disabled} + .required=${this.required} .label=${this.label === undefined && this.hass ? this.hass.localize("ui.components.area-picker.area") : this.label} diff --git a/src/components/ha-areas-picker.ts b/src/components/ha-areas-picker.ts new file mode 100644 index 0000000000..24b4e4c2b0 --- /dev/null +++ b/src/components/ha-areas-picker.ts @@ -0,0 +1,163 @@ +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; +import type { EntityRegistryEntry } from "../data/entity_registry"; +import { SubscribeMixin } from "../mixins/subscribe-mixin"; +import type { HomeAssistant } from "../types"; +import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; +import "./ha-area-picker"; + +@customElement("ha-areas-picker") +export class HaAreasPicker extends SubscribeMixin(LitElement) { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public label?: string; + + @property() public value?: string[]; + + @property() public placeholder?: string; + + @property({ type: Boolean, attribute: "no-add" }) + public noAdd?: boolean; + + /** + * Show only areas with entities from specific domains. + * @type {Array} + * @attr include-domains + */ + @property({ type: Array, attribute: "include-domains" }) + public includeDomains?: string[]; + + /** + * Show no areas with entities of these domains. + * @type {Array} + * @attr exclude-domains + */ + @property({ type: Array, attribute: "exclude-domains" }) + public excludeDomains?: string[]; + + /** + * Show only areas with entities of these device classes. + * @type {Array} + * @attr include-device-classes + */ + @property({ type: Array, attribute: "include-device-classes" }) + public includeDeviceClasses?: string[]; + + @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; + + @property() public entityFilter?: (entity: EntityRegistryEntry) => boolean; + + @property({ attribute: "picked-area-label" }) + public pickedAreaLabel?: string; + + @property({ attribute: "pick-area-label" }) + public pickAreaLabel?: string; + + @property({ type: Boolean }) public disabled?: boolean; + + @property({ type: Boolean }) public required?: boolean; + + protected render(): TemplateResult { + if (!this.hass) { + return html``; + } + + const currentAreas = this._currentAreas; + return html` + ${currentAreas.map( + (area) => html` +
+ +
+ ` + )} +
+ +
+ `; + } + + private get _currentAreas(): string[] { + return this.value || []; + } + + private async _updateAreas(areas) { + this.value = areas; + + fireEvent(this, "value-changed", { + value: areas, + }); + } + + private _areaChanged(ev: CustomEvent) { + ev.stopPropagation(); + const curValue = (ev.currentTarget as any).curValue; + const newValue = ev.detail.value; + if (newValue === curValue) { + return; + } + const currentAreas = this._currentAreas; + if (!newValue || currentAreas.includes(newValue)) { + this._updateAreas(currentAreas.filter((ent) => ent !== curValue)); + return; + } + this._updateAreas( + currentAreas.map((ent) => (ent === curValue ? newValue : ent)) + ); + } + + private _addArea(ev: CustomEvent) { + ev.stopPropagation(); + + const toAdd = ev.detail.value; + if (!toAdd) { + return; + } + (ev.currentTarget as any).value = ""; + const currentAreas = this._currentAreas; + if (currentAreas.includes(toAdd)) { + return; + } + + this._updateAreas([...currentAreas, toAdd]); + } + + static override styles = css` + div { + margin-top: 8px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-areas-picker": HaAreasPicker; + } +} diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts index 64b12f18e2..c54739d65d 100644 --- a/src/components/ha-base-time-input.ts +++ b/src/components/ha-base-time-input.ts @@ -1,12 +1,13 @@ -import { LitElement, html, TemplateResult, css } from "lit"; -import { customElement, property } from "lit/decorators"; -import "./ha-select"; import "@material/mwc-list/mwc-list-item"; -import "./ha-textfield"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import { stopPropagation } from "../common/dom/stop_propagation"; +import "./ha-select"; +import "./ha-textfield"; export interface TimeChangedEvent { + days?: number; hours: number; minutes: number; seconds: number; @@ -21,6 +22,11 @@ export class HaBaseTimeInput extends LitElement { */ @property() label?: string; + /** + * Helper for the input + */ + @property() helper?: string; + /** * auto validate time inputs */ @@ -41,6 +47,11 @@ export class HaBaseTimeInput extends LitElement { */ @property({ type: Boolean }) disabled = false; + /** + * day + */ + @property({ type: Number }) days = 0; + /** * hour */ @@ -61,6 +72,11 @@ export class HaBaseTimeInput extends LitElement { */ @property({ type: Number }) milliseconds = 0; + /** + * Label for the day input + */ + @property() dayLabel = ""; + /** * Label for the hour input */ @@ -91,6 +107,11 @@ export class HaBaseTimeInput extends LitElement { */ @property({ type: Boolean }) enableMillisecond = false; + /** + * show the day field + */ + @property({ type: Boolean }) enableDay = false; + /** * limit hours input */ @@ -108,8 +129,33 @@ export class HaBaseTimeInput extends LitElement { protected render(): TemplateResult { return html` - ${this.label ? html`` : ""} + ${this.label + ? html`` + : ""}
+ ${this.enableDay + ? html` + + + ` + : ""} + PM `}
+ ${this.helper ? html`
${this.helper}
` : ""} `; } @@ -303,6 +350,13 @@ export class HaBaseTimeInput extends LitElement { color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87)); padding-left: 4px; } + + .helper { + color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6)); + font-size: 0.75rem; + padding-left: 16px; + padding-right: 16px; + } `; } diff --git a/src/components/ha-chip-set.ts b/src/components/ha-chip-set.ts index 2e659c8353..699f1b0a73 100644 --- a/src/components/ha-chip-set.ts +++ b/src/components/ha-chip-set.ts @@ -25,13 +25,7 @@ export class HaChipSet extends LitElement { ${unsafeCSS(chipStyles)} slot::slotted(ha-chip) { - margin: 4px; - } - slot::slotted(ha-chip:first-of-type) { - margin-left: -4px; - } - slot::slotted(ha-chip:last-of-type) { - margin-right: -4px; + margin: 4px 4px 4px 0; } `; } diff --git a/src/components/ha-chip.ts b/src/components/ha-chip.ts index 2a41ffc760..f22e0e29ad 100644 --- a/src/components/ha-chip.ts +++ b/src/components/ha-chip.ts @@ -14,6 +14,8 @@ import { customElement, property } from "lit/decorators"; export class HaChip extends LitElement { @property({ type: Boolean }) public hasIcon = false; + @property({ type: Boolean }) public hasTrailingIcon = false; + @property({ type: Boolean }) public noText = false; protected render(): TemplateResult { @@ -30,6 +32,11 @@ export class HaChip extends LitElement { + ${this.hasTrailingIcon + ? html`
+ +
` + : null} `; } @@ -53,14 +60,20 @@ export class HaChip extends LitElement { color: var(--ha-chip-text-color, var(--primary-text-color)); } - .mdc-chip__icon--leading { - --mdc-icon-size: 20px; + .mdc-chip__icon--leading, + .mdc-chip__icon--trailing { + --mdc-icon-size: 18px; + line-height: 14px; color: var(--ha-chip-icon-color, var(--ha-chip-text-color)); } .mdc-chip.no-text .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) { margin-right: -4px; } + + span[role="gridcell"] { + line-height: 14px; + } `; } } diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index f8f1bd5cde..c1bc266ed9 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -87,6 +87,8 @@ export class HaComboBox extends LitElement { @property({ type: Boolean }) public disabled?: boolean; + @property({ type: Boolean }) public required?: boolean; + @property({ type: Boolean, reflect: true, attribute: "opened" }) private _opened?: boolean; @@ -108,17 +110,22 @@ export class HaComboBox extends LitElement { return this._comboBox.selectedItem; } + public setInputValue(value: string) { + this._comboBox.value = value; + } + protected render(): TemplateResult { return html` `; diff --git a/src/components/ha-duration-input.ts b/src/components/ha-duration-input.ts index 9ac4e72e2f..408d1e85c7 100644 --- a/src/components/ha-duration-input.ts +++ b/src/components/ha-duration-input.ts @@ -5,6 +5,7 @@ import "./ha-base-time-input"; import type { TimeChangedEvent } from "./ha-base-time-input"; export interface HaDurationData { + days?: number; hours?: number; minutes?: number; seconds?: number; @@ -17,10 +18,14 @@ class HaDurationInput extends LitElement { @property() public label?: string; + @property() public helper?: string; + @property({ type: Boolean }) public required?: boolean; @property({ type: Boolean }) public enableMillisecond?: boolean; + @property({ type: Boolean }) public enableDay?: boolean; + @property({ type: Boolean }) public disabled = false; @query("paper-time-input", true) private _input?: HTMLElement; @@ -35,19 +40,23 @@ class HaDurationInput extends LitElement { return html` 24) { + value.days = (value.days ?? 0) + Math.floor(value.hours / 24); + value.hours %= 24; + } + fireEvent(this, "value-changed", { value, }); diff --git a/src/components/ha-form/compute-initial-ha-form-data.ts b/src/components/ha-form/compute-initial-ha-form-data.ts index 0e80433c74..b70148fa86 100644 --- a/src/components/ha-form/compute-initial-ha-form-data.ts +++ b/src/components/ha-form/compute-initial-ha-form-data.ts @@ -1,4 +1,5 @@ -import { HaFormSchema } from "./types"; +import type { Selector } from "../../data/selector"; +import type { HaFormSchema } from "./types"; export const computeInitialHaFormData = ( schema: HaFormSchema[] @@ -31,6 +32,55 @@ export const computeInitialHaFormData = ( minutes: 0, seconds: 0, }; + } else if ("selector" in field) { + const selector: Selector = field.selector; + + if ("device" in selector) { + data[field.name] = selector.device.multiple ? [] : ""; + } else if ("entity" in selector) { + data[field.name] = selector.entity.multiple ? [] : ""; + } else if ("area" in selector) { + data[field.name] = selector.area.multiple ? [] : ""; + } else if ("boolean" in selector) { + data[field.name] = false; + } else if ( + "text" in selector || + "addon" in selector || + "attribute" in selector || + "icon" in selector || + "theme" in selector + ) { + data[field.name] = ""; + } else if ("number" in selector) { + data[field.name] = selector.number.min ?? 0; + } else if ("select" in selector) { + if (selector.select.options.length) { + data[field.name] = selector.select.options[0][0]; + } + } else if ("duration" in selector) { + data[field.name] = { + hours: 0, + minutes: 0, + seconds: 0, + }; + } else if ("time" in selector) { + data[field.name] = "00:00:00"; + } else if ("date" in selector || "datetime" in selector) { + const now = new Date().toISOString().slice(0, 10); + data[field.name] = `${now} 00:00:00`; + } else if ("color_rgb" in selector) { + data[field.name] = [0, 0, 0]; + } else if ("color_temp" in selector) { + data[field.name] = selector.color_temp.min_mireds ?? 153; + } else if ( + "action" in selector || + "media" in selector || + "target" in selector + ) { + data[field.name] = {}; + } else { + throw new Error("Selector not supported in initial form data"); + } } }); return data; diff --git a/src/components/ha-form/ha-form-select.ts b/src/components/ha-form/ha-form-select.ts index 2e342a39a2..73dc74b95c 100644 --- a/src/components/ha-form/ha-form-select.ts +++ b/src/components/ha-form/ha-form-select.ts @@ -1,16 +1,20 @@ -import "@material/mwc-list/mwc-list-item"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; -import { stopPropagation } from "../../common/dom/stop_propagation"; -import "../ha-radio"; -import type { HaRadio } from "../ha-radio"; -import "../ha-select"; -import type { HaSelect } from "../ha-select"; -import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types"; +import type { HomeAssistant } from "../../types"; +import type { + HaFormElement, + HaFormSelectData, + HaFormSelectSchema, +} from "./types"; +import type { SelectSelector } from "../../data/selector"; +import "../ha-selector/ha-selector-select"; @customElement("ha-form-select") export class HaFormSelect extends LitElement implements HaFormElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public schema!: HaFormSelectSchema; @property() public data!: HaFormSelectData; @@ -19,60 +23,35 @@ export class HaFormSelect extends LitElement implements HaFormElement { @property({ type: Boolean }) public disabled = false; - @query("ha-select", true) private _input?: HTMLElement; - - public focus() { - if (this._input) { - this._input.focus(); - } - } + private _selectSchema = memoizeOne( + (options): SelectSelector => ({ + select: { + options: options.map((option) => ({ + value: option[0], + label: option[1], + })), + }, + }) + ); protected render(): TemplateResult { - if (this.schema.required && this.schema.options!.length < 6) { - return html` -
- ${this.label} - ${this.schema.options.map( - ([value, label]) => html` - - - - ` - )} -
- `; - } - return html` - - ${!this.schema.required - ? html`` - : ""} - ${this.schema.options!.map( - ([value, label]) => html` - ${label} - ` - )} - + .required=${this.schema.required} + .selector=${this._selectSchema(this.schema.options)} + @value-changed=${this._valueChanged} + > `; } private _valueChanged(ev: CustomEvent) { ev.stopPropagation(); - let value: string | undefined = (ev.target as HaSelect | HaRadio).value; + let value: string | undefined = ev.detail.value; if (value === this.data) { return; @@ -86,15 +65,6 @@ export class HaFormSelect extends LitElement implements HaFormElement { value, }); } - - static get styles(): CSSResultGroup { - return css` - ha-select, - mwc-formfield { - display: block; - } - `; - } } declare global { diff --git a/src/components/ha-form/ha-form-string.ts b/src/components/ha-form/ha-form-string.ts index e82a96519e..f527676aaf 100644 --- a/src/components/ha-form/ha-form-string.ts +++ b/src/components/ha-form/ha-form-string.ts @@ -28,6 +28,8 @@ export class HaFormString extends LitElement implements HaFormElement { @property() public label!: string; + @property() public helper?: string; + @property({ type: Boolean }) public disabled = false; @state() private _unmaskedPassword = false; @@ -53,6 +55,8 @@ export class HaFormString extends LitElement implements HaFormElement { : "password"} .label=${this.label} .value=${this.data || ""} + .helper=${this.helper} + helperPersistent .disabled=${this.disabled} .required=${this.schema.required} .autoValidate=${this.schema.required} diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index e1e87d86f9..f3db63e2c1 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -106,6 +106,7 @@ export class HaForm extends LitElement implements HaFormElement { .disabled=${this.disabled} .helper=${this._computeHelper(item)} .required=${item.required || false} + .context=${this._generateContext(item)} >` : dynamicElement(`ha-form-${item.type}`, { schema: item, @@ -115,6 +116,7 @@ export class HaForm extends LitElement implements HaFormElement { hass: this.hass, computeLabel: this.computeLabel, computeHelper: this.computeHelper, + context: this._generateContext(item), })} `; })} @@ -122,6 +124,20 @@ export class HaForm extends LitElement implements HaFormElement { `; } + private _generateContext( + schema: HaFormSchema + ): Record | undefined { + if (!schema.context) { + return undefined; + } + + const context = {}; + for (const [context_key, data_key] of Object.entries(schema.context)) { + context[context_key] = this.data[data_key]; + } + return context; + } + protected createRenderRoot() { const root = super.createRenderRoot(); // attach it as soon as possible to make sure we fetch all events. diff --git a/src/components/ha-form/types.ts b/src/components/ha-form/types.ts index 2c3a9912d4..d8e380cca4 100644 --- a/src/components/ha-form/types.ts +++ b/src/components/ha-form/types.ts @@ -24,6 +24,7 @@ export interface HaFormBaseSchema { // This value will be set initially when form is loaded suggested_value?: HaFormData; }; + context?: Record; } export interface HaFormGridSchema extends HaFormBaseSchema { diff --git a/src/components/ha-icon-picker.ts b/src/components/ha-icon-picker.ts index 57ec368cdc..719976bde2 100644 --- a/src/components/ha-icon-picker.ts +++ b/src/components/ha-icon-picker.ts @@ -39,6 +39,8 @@ export class HaIconPicker extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = false; + @property({ type: Boolean }) public invalid = false; @state() private _opened = false; @@ -56,6 +58,7 @@ export class HaIconPicker extends LitElement { .filteredItems=${iconItems} .label=${this.label} .disabled=${this.disabled} + .required=${this.required} .placeholder=${this.placeholder} .errorMessage=${this.errorMessage} .invalid=${this.invalid} diff --git a/src/components/ha-labeled-slider.js b/src/components/ha-labeled-slider.js index f029b2f21f..af7134ab13 100644 --- a/src/components/ha-labeled-slider.js +++ b/src/components/ha-labeled-slider.js @@ -33,7 +33,7 @@ class HaLabeledSlider extends PolymerElement { } -
[[caption]]
+
[[_getTitle()]]
@@ -49,10 +49,15 @@ class HaLabeledSlider extends PolymerElement { `; } + _getTitle() { + return `${this.caption}${this.required ? "*" : ""}`; + } + static get properties() { return { caption: String, disabled: Boolean, + required: Boolean, min: Number, max: Number, pin: Boolean, diff --git a/src/components/ha-markdown.ts b/src/components/ha-markdown.ts index 2b628c1e04..4183fba672 100644 --- a/src/components/ha-markdown.ts +++ b/src/components/ha-markdown.ts @@ -2,6 +2,11 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import "./ha-markdown-element"; +// Import components that are allwoed to be defined. +import "./ha-alert"; +import "./ha-icon"; +import "./ha-svg-icon"; + @customElement("ha-markdown") export class HaMarkdown extends LitElement { @property() public content?; diff --git a/src/components/ha-selector/ha-selector-addon.ts b/src/components/ha-selector/ha-selector-addon.ts index 47bb9c045b..acabb24cf5 100644 --- a/src/components/ha-selector/ha-selector-addon.ts +++ b/src/components/ha-selector/ha-selector-addon.ts @@ -14,11 +14,17 @@ export class HaAddonSelector extends LitElement { @property() public label?: string; + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + protected render() { return html``; } diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index 5f307dd013..8aaada7e23 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -6,6 +6,7 @@ import { EntityRegistryEntry } from "../../data/entity_registry"; import { AreaSelector } from "../../data/selector"; import { HomeAssistant } from "../../types"; import "../ha-area-picker"; +import "../ha-areas-picker"; @customElement("ha-selector-area") export class HaAreaSelector extends LitElement { @@ -21,6 +22,8 @@ export class HaAreaSelector extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected updated(changedProperties) { if (changedProperties.has("selector")) { const oldSelector = changedProperties.get("selector"); @@ -28,27 +31,55 @@ export class HaAreaSelector extends LitElement { oldSelector !== this.selector && this.selector.area.device?.integration ) { - this._loadConfigEntries(); + getConfigEntries(this.hass, { + domain: this.selector.area.device.integration, + }).then((entries) => { + this._configEntries = entries; + }); } } } protected render() { - return html``; + if (!this.selector.area.multiple) { + return html` + + `; + } + + return html` + + `; } private _filterEntities = (entity: EntityRegistryEntry): boolean => { @@ -85,12 +116,6 @@ export class HaAreaSelector extends LitElement { } return true; }; - - private async _loadConfigEntries() { - this._configEntries = (await getConfigEntries(this.hass)).filter( - (entry) => entry.domain === this.selector.area.device?.integration - ); - } } declare global { diff --git a/src/components/ha-selector/ha-selector-attribute.ts b/src/components/ha-selector/ha-selector-attribute.ts index 5739089061..06baa1e6b6 100644 --- a/src/components/ha-selector/ha-selector-attribute.ts +++ b/src/components/ha-selector/ha-selector-attribute.ts @@ -1,9 +1,10 @@ -import "../entity/ha-entity-attribute-picker"; -import { html, LitElement } from "lit"; +import { html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; import { AttributeSelector } from "../../data/selector"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; +import "../entity/ha-entity-attribute-picker"; @customElement("ha-selector-attribute") export class HaSelectorAttribute extends SubscribeMixin(LitElement) { @@ -17,18 +18,67 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = true; + + @property() public context?: { + filter_entity?: string; + }; + protected render() { return html` `; } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if ( + // No need to filter value if no value + !this.value || + // Only adjust value if we used the context + this.selector.attribute.entity_id || + // Only check if context has changed + !changedProps.has("context") + ) { + return; + } + + const oldContext = changedProps.get("context") as this["context"]; + + if ( + !this.context || + oldContext?.filter_entity === this.context.filter_entity + ) { + return; + } + + // Validate that that the attribute is still valid for this entity, else unselect. + let invalid = false; + if (this.context.filter_entity) { + const stateObj = this.hass.states[this.context.filter_entity]; + + if (!(stateObj && this.value in stateObj.attributes)) { + invalid = true; + } + } else { + invalid = this.value !== undefined; + } + + if (invalid) { + fireEvent(this, "value-changed", { + value: undefined, + }); + } + } } declare global { diff --git a/src/components/ha-selector/ha-selector-color-rgb.ts b/src/components/ha-selector/ha-selector-color-rgb.ts new file mode 100644 index 0000000000..780d83c832 --- /dev/null +++ b/src/components/ha-selector/ha-selector-color-rgb.ts @@ -0,0 +1,62 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { hex2rgb, rgb2hex } from "../../common/color/convert-color"; +import { fireEvent } from "../../common/dom/fire_event"; +import type { ColorRGBSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; +import "../ha-textfield"; + +@customElement("ha-selector-color_rgb") +export class HaColorRGBSelector extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public selector!: ColorRGBSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + protected render() { + return html` + + `; + } + + private _valueChanged(ev: CustomEvent) { + const value = (ev.target as any).value; + fireEvent(this, "value-changed", { + value: hex2rgb(value), + }); + } + + static styles = css` + :host { + display: flex; + justify-content: flex-end; + align-items: center; + } + ha-textfield { + --text-field-padding: 8px; + min-width: 75px; + flex-grow: 1; + margin: 0 4px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-color_rgb": HaColorRGBSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-color-temp.ts b/src/components/ha-selector/ha-selector-color-temp.ts new file mode 100644 index 0000000000..67ac8abcc7 --- /dev/null +++ b/src/components/ha-selector/ha-selector-color-temp.ts @@ -0,0 +1,62 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import type { ColorTempSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; +import "../ha-labeled-slider"; + +@customElement("ha-selector-color_temp") +export class HaColorTempSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: ColorTempSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + protected render() { + return html` + + `; + } + + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { + value: Number((ev.target as any).value), + }); + } + + static styles = css` + ha-labeled-slider { + --ha-slider-background: -webkit-linear-gradient( + right, + rgb(255, 160, 0) 0%, + white 50%, + rgb(166, 209, 255) 100% + ); + /* The color temp minimum value shouldn't be rendered differently. It's not "off". */ + --paper-slider-knob-start-border-color: var(--primary-color); + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-color_temp": HaColorTempSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-date.ts b/src/components/ha-selector/ha-selector-date.ts new file mode 100644 index 0000000000..47fa5b7d91 --- /dev/null +++ b/src/components/ha-selector/ha-selector-date.ts @@ -0,0 +1,39 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import type { DateSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; +import "../ha-date-input"; + +@customElement("ha-selector-date") +export class HaDateSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: DateSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + protected render() { + return html` + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-date": HaDateSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-datetime.ts b/src/components/ha-selector/ha-selector-datetime.ts new file mode 100644 index 0000000000..c8d1dab2d3 --- /dev/null +++ b/src/components/ha-selector/ha-selector-datetime.ts @@ -0,0 +1,78 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import type { DateTimeSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; +import "../ha-date-input"; +import type { HaDateInput } from "../ha-date-input"; +import "../ha-time-input"; +import type { HaTimeInput } from "../ha-time-input"; + +@customElement("ha-selector-datetime") +export class HaDateTimeSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: DateTimeSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + @query("ha-date-input") private _dateInput!: HaDateInput; + + @query("ha-time-input") private _timeInput!: HaTimeInput; + + protected render() { + const values = this.value?.split(" "); + + return html` + + + + `; + } + + private _valueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: `${this._dateInput.value} ${this._timeInput.value}`, + }); + } + + static styles = css` + :host { + display: flex; + align-items: center; + flex-direction: row; + } + + ha-date-input { + min-width: 150px; + margin-right: 4px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-datetime": HaDateTimeSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index 60da624665..efb5dee013 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -1,10 +1,11 @@ import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { ConfigEntry, getConfigEntries } from "../../data/config_entries"; -import { DeviceRegistryEntry } from "../../data/device_registry"; -import { DeviceSelector } from "../../data/selector"; -import { HomeAssistant } from "../../types"; +import type { DeviceRegistryEntry } from "../../data/device_registry"; +import type { DeviceSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; import "../device/ha-device-picker"; +import "../device/ha-devices-picker"; @customElement("ha-selector-device") export class HaDeviceSelector extends LitElement { @@ -20,30 +21,56 @@ export class HaDeviceSelector extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected updated(changedProperties) { if (changedProperties.has("selector")) { const oldSelector = changedProperties.get("selector"); if (oldSelector !== this.selector && this.selector.device?.integration) { - this._loadConfigEntries(); + getConfigEntries(this.hass, { + domain: this.selector.device.integration, + }).then((entries) => { + this._configEntries = entries; + }); } } } protected render() { - return html``; + if (!this.selector.device.multiple) { + return html` + + `; + } + + return html` + ${this.label ? html`` : ""} + + `; } private _filterDevices = (device: DeviceRegistryEntry): boolean => { @@ -71,12 +98,6 @@ export class HaDeviceSelector extends LitElement { } return true; }; - - private async _loadConfigEntries() { - this._configEntries = (await getConfigEntries(this.hass)).filter( - (entry) => entry.domain === this.selector.device.integration - ); - } } declare global { diff --git a/src/components/ha-selector/ha-selector-duration.ts b/src/components/ha-selector/ha-selector-duration.ts index 1471750d90..61bde6fa21 100644 --- a/src/components/ha-selector/ha-selector-duration.ts +++ b/src/components/ha-selector/ha-selector-duration.ts @@ -1,8 +1,8 @@ -import "../ha-duration-input"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { DurationSelector } from "../../data/selector"; -import { HomeAssistant } from "../../types"; +import type { DurationSelector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; +import "../ha-duration-input"; @customElement("ha-selector-duration") export class HaTimeDuration extends LitElement { @@ -14,6 +14,8 @@ export class HaTimeDuration extends LitElement { @property() public label?: string; + @property() public helper?: string; + @property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public required = true; @@ -22,9 +24,11 @@ export class HaTimeDuration extends LitElement { return html` `; } diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 566dfc0a45..84f060ea0d 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -1,20 +1,23 @@ -import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; -import { html, LitElement } from "lit"; +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 { subscribeEntityRegistry } from "../../data/entity_registry"; +import { + EntitySources, + fetchEntitySourcesWithCache, +} from "../../data/entity_sources"; import { EntitySelector } from "../../data/selector"; -import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../types"; +import "../entity/ha-entities-picker"; import "../entity/ha-entity-picker"; @customElement("ha-selector-entity") -export class HaEntitySelector extends SubscribeMixin(LitElement) { +export class HaEntitySelector extends LitElement { @property() public hass!: HomeAssistant; @property() public selector!: EntitySelector; - @state() private _entityPlaformLookup?: Record; + @state() private _entitySources?: EntitySources; @property() public value?: any; @@ -22,60 +25,77 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected render() { - return html``; + if (!this.selector.entity.multiple) { + return html``; + } + + return html` + ${this.label ? html`` : ""} + + `; } - 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; - }), - ]; + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if ( + changedProps.has("selector") && + this.selector.entity.integration && + !this._entitySources + ) { + fetchEntitySourcesWithCache(this.hass).then((sources) => { + this._entitySources = sources; + }); + } } private _filterEntities = (entity: HassEntity): boolean => { - if (this.selector.entity?.domain) { - const filterDomain = this.selector.entity.domain; - const filterDomainIsArray = Array.isArray(filterDomain); + const { + domain: filterDomain, + device_class: filterDeviceClass, + integration: filterIntegration, + } = this.selector.entity; + + if (filterDomain) { const entityDomain = computeStateDomain(entity); if ( - (filterDomainIsArray && !filterDomain.includes(entityDomain)) || - (!filterDomainIsArray && entityDomain !== filterDomain) + Array.isArray(filterDomain) + ? !filterDomain.includes(entityDomain) + : entityDomain !== filterDomain ) { return false; } } - if (this.selector.entity?.device_class) { - if ( - !entity.attributes.device_class || - entity.attributes.device_class !== this.selector.entity.device_class - ) { - return false; - } + if ( + filterDeviceClass && + entity.attributes.device_class !== filterDeviceClass + ) { + return false; } - if (this.selector.entity?.integration) { - if ( - !this._entityPlaformLookup || - this._entityPlaformLookup[entity.entity_id] !== - this.selector.entity.integration - ) { - return false; - } + if ( + filterIntegration && + this._entitySources?.[entity.entity_id]?.domain !== filterIntegration + ) { + return false; } return true; }; diff --git a/src/components/ha-selector/ha-selector-icon.ts b/src/components/ha-selector/ha-selector-icon.ts index 0e4a712588..69e11ed0ad 100644 --- a/src/components/ha-selector/ha-selector-icon.ts +++ b/src/components/ha-selector/ha-selector-icon.ts @@ -1,9 +1,9 @@ -import "../ha-icon-picker"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { HomeAssistant } from "../../types"; -import { IconSelector } from "../../data/selector"; import { fireEvent } from "../../common/dom/fire_event"; +import { IconSelector } from "../../data/selector"; +import { HomeAssistant } from "../../types"; +import "../ha-icon-picker"; @customElement("ha-selector-icon") export class HaIconSelector extends LitElement { @@ -17,11 +17,15 @@ export class HaIconSelector extends LitElement { @property({ type: Boolean, reflect: true }) public disabled = false; + @property({ type: Boolean }) public required = true; + protected render() { return html` ) { @@ -84,6 +86,7 @@ export class HaMediaSelector extends LitElement { .label=${this.label || this.hass.localize("ui.components.selectors.media.pick_media_player")} .disabled=${this.disabled} + .required=${this.required} include-domains='["media_player"]' allow-custom-entity @value-changed=${this._entityChanged} diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index 0094192f1b..84e8f0e12f 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -19,13 +19,15 @@ export class HaNumberSelector extends LitElement { @property() public label?: string; + @property() public helper?: string; + @property({ type: Boolean }) public required = true; @property({ type: Boolean }) public disabled = false; protected render() { return html`${this.selector.number.mode !== "box" - ? html`${this.label} - ${this.selector.select.options.map((item: string | SelectOption) => { - const value = typeof item === "object" ? item.value : item; - const label = typeof item === "object" ? item.label : item; + @property({ type: Boolean }) public required = true; - return html`${label}`; - })} - `; + @query("ha-combo-box", true) private comboBox!: HaComboBox; + + private _filter = ""; + + protected render() { + const options = this.selector.select.options.map((option) => + typeof option === "object" ? option : { value: option, label: option } + ); + + if (!this.selector.select.custom_value && this._mode === "list") { + if (!this.selector.select.multiple || this.required) { + return html` +
+ ${this.label} + ${options.map( + (item: SelectOption) => html` + + + + ` + )} +
+ `; + } + + return html` +
+ ${this.label}${options.map( + (item: SelectOption) => html` + + + + ` + )} +
+ `; + } + + if (this.selector.select.multiple) { + const value = + !this.value || this.value === "" ? [] : (this.value as string[]); + + return html` + + ${value?.map( + (item, idx) => + html` + + ${options.find((option) => option.value === item)?.label || + item} + + + ` + )} + + + !this.value?.includes(item.value))} + @filter-changed=${this._filterChanged} + @value-changed=${this._comboBoxValueChanged} + > + `; + } + + if (this.selector.select.custom_value) { + if ( + this.value !== undefined && + !options.find((option) => option.value === this.value) + ) { + options.unshift({ value: this.value, label: this.value }); + } + + return html` + + `; + } + + return html` + + ${options.map( + (item: SelectOption) => html` + ${item.label} + ` + )} + + `; + } + + private get _mode(): "list" | "dropdown" { + return ( + this.selector.select.mode || + (this.selector.select.options.length < 6 ? "list" : "dropdown") + ); } private _valueChanged(ev) { ev.stopPropagation(); - if (this.disabled || !ev.target.value) { + const value = ev.detail?.value || ev.target.value; + if (this.disabled || !value) { return; } fireEvent(this, "value-changed", { - value: ev.target.value, + value: value, }); } - static get styles(): CSSResultGroup { - return css` - ha-select { - width: 100%; + private _checkboxChanged(ev) { + ev.stopPropagation(); + if (this.disabled) { + return; + } + + let newValue: string[]; + const value: string = ev.target.value; + const checked = ev.target.checked; + + if (checked) { + if (!this.value) { + newValue = [value]; + } else if (this.value.includes(value)) { + return; + } else { + newValue = [...this.value, value]; } - `; + } else { + if (!this.value?.includes(value)) { + return; + } + newValue = (this.value as string[]).filter((v) => v !== value); + } + + fireEvent(this, "value-changed", { + value: newValue, + }); } + + private async _removeItem(ev) { + const value: string[] = [...(this.value! as string[])]; + value.splice(ev.target.idx, 1); + + fireEvent(this, "value-changed", { + value, + }); + await this.updateComplete; + this._filterChanged(); + } + + private _comboBoxValueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const newValue = ev.detail.value; + + if (this.disabled || newValue === "") { + return; + } + + if (!this.selector.select.multiple) { + fireEvent(this, "value-changed", { + value: newValue, + }); + return; + } + + if (newValue !== undefined && this.value?.includes(newValue)) { + return; + } + + setTimeout(() => { + this._filterChanged(); + this.comboBox.setInputValue(""); + }, 0); + + const currentValue = + !this.value || this.value === "" ? [] : (this.value as string[]); + + fireEvent(this, "value-changed", { + value: [...currentValue, newValue], + }); + } + + private _filterChanged(ev?: CustomEvent): void { + this._filter = ev?.detail.value || ""; + + const filteredItems = this.comboBox.items?.filter((item) => { + if (this.selector.select.multiple && this.value?.includes(item.value)) { + return false; + } + const label = item.label || item.value; + return label.toLowerCase().includes(this._filter?.toLowerCase()); + }); + + if (this._filter && this.selector.select.custom_value) { + filteredItems?.unshift({ label: this._filter, value: this._filter }); + } + + this.comboBox.filteredItems = filteredItems; + } + + static styles = css` + ha-select, + mwc-formfield, + ha-formfield { + display: block; + } + `; } declare global { diff --git a/src/components/ha-selector/ha-selector-target.ts b/src/components/ha-selector/ha-selector-target.ts index 4d81be67df..cf25317cce 100644 --- a/src/components/ha-selector/ha-selector-target.ts +++ b/src/components/ha-selector/ha-selector-target.ts @@ -134,9 +134,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { private async _loadConfigEntries() { this._configEntries = (await getConfigEntries(this.hass)).filter( (entry) => - entry.domain === - (this.selector.target.device?.integration || - this.selector.target.entity?.integration) + entry.domain === this.selector.target.device?.integration || + entry.domain === this.selector.target.entity?.integration ); } diff --git a/src/components/ha-selector/ha-selector-text.ts b/src/components/ha-selector/ha-selector-text.ts index 2e6c879d9e..b91e0edc33 100644 --- a/src/components/ha-selector/ha-selector-text.ts +++ b/src/components/ha-selector/ha-selector-text.ts @@ -18,6 +18,8 @@ export class HaTextSelector extends LitElement { @property() public placeholder?: string; + @property() public helper?: string; + @property() public selector!: StringSelector; @property({ type: Boolean }) public disabled = false; @@ -32,6 +34,8 @@ export class HaTextSelector extends LitElement { .label=${this.label} .placeholder=${this.placeholder} .value=${this.value || ""} + .helper=${this.helper} + helperPersistent .disabled=${this.disabled} @input=${this._handleChange} autocapitalize="none" @@ -44,6 +48,8 @@ export class HaTextSelector extends LitElement { return html` + .disabled=${this.disabled} + .required=${this.required} + > `; } } diff --git a/src/components/ha-selector/ha-selector-time.ts b/src/components/ha-selector/ha-selector-time.ts index ff829ff12b..caf2ee0d4d 100644 --- a/src/components/ha-selector/ha-selector-time.ts +++ b/src/components/ha-selector/ha-selector-time.ts @@ -16,12 +16,15 @@ export class HaTimeSelector extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = false; + protected render() { return html` diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 61b790cd98..09a234e55c 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -8,6 +8,9 @@ import "./ha-selector-addon"; import "./ha-selector-area"; import "./ha-selector-attribute"; import "./ha-selector-boolean"; +import "./ha-selector-color-rgb"; +import "./ha-selector-date"; +import "./ha-selector-datetime"; import "./ha-selector-device"; import "./ha-selector-duration"; import "./ha-selector-entity"; @@ -21,6 +24,7 @@ import "./ha-selector-icon"; import "./ha-selector-media"; import "./ha-selector-theme"; import "./ha-selector-location"; +import "./ha-selector-color-temp"; @customElement("ha-selector") export class HaSelector extends LitElement { @@ -40,6 +44,8 @@ export class HaSelector extends LitElement { @property({ type: Boolean }) public required = true; + @property() public context?: Record; + public focus() { this.shadowRoot?.getElementById("selector")?.focus(); } @@ -59,6 +65,7 @@ export class HaSelector extends LitElement { disabled: this.disabled, required: this.required, helper: this.helper, + context: this.context, id: "selector", })} `; diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts index ee1692e567..438adb66d4 100644 --- a/src/components/ha-service-control.ts +++ b/src/components/ha-service-control.ts @@ -471,6 +471,7 @@ export class HaServiceControl extends LitElement { } ha-settings-row { --paper-time-input-justify-content: flex-end; + --settings-row-content-width: 100%; border-top: var( --service-control-items-border-top, 1px solid var(--divider-color) @@ -489,9 +490,6 @@ export class HaServiceControl extends LitElement { margin: var(--service-control-padding, 0 16px); padding: 16px 0; } - :host(:not([narrow])) ha-settings-row ha-selector { - width: 60%; - } .checkbox-spacer { width: 32px; } diff --git a/src/components/ha-settings-row.ts b/src/components/ha-settings-row.ts index a28738b265..252bb80dc8 100644 --- a/src/components/ha-settings-row.ts +++ b/src/components/ha-settings-row.ts @@ -21,7 +21,7 @@ export class HaSettingsRow extends LitElement {
- +
`; } @@ -43,6 +43,18 @@ export class HaSettingsRow extends LitElement { ); flex: 1; } + .content { + display: contents; + } + :host(:not([narrow])) .content { + display: flex; + justify-content: flex-end; + flex: 1; + padding: 16px 0; + } + .content ::slotted(*) { + width: var(--settings-row-content-width); + } :host([narrow]) { align-items: normal; flex-direction: column; diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 70ee6ed471..b5e1ba55f5 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -37,6 +37,7 @@ import { LocalStorage } from "../common/decorators/local-storage"; import { fireEvent } from "../common/dom/fire_event"; import { toggleAttribute } from "../common/dom/toggle_attribute"; import { computeDomain } from "../common/entity/compute_domain"; +import { computeStateDomain } from "../common/entity/compute_state_domain"; import { stringCompare } from "../common/string/compare"; import { computeRTL } from "../common/util/compute_rtl"; import { ActionHandlerDetail } from "../data/lovelace"; @@ -44,6 +45,7 @@ import { PersistentNotification, subscribeNotifications, } from "../data/persistent_notification"; +import { updateCanInstall, UpdateEntity } from "../data/update"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant, PanelInfo, Route } from "../types"; @@ -68,7 +70,6 @@ const SORT_VALUE_URL_PATHS = { const PANEL_ICONS = { calendar: mdiCalendar, - config: mdiCog, "developer-tools": mdiHammer, energy: mdiLightningBolt, history: mdiChartBox, @@ -190,6 +191,8 @@ class HaSidebar extends LitElement { @state() private _notifications?: PersistentNotification[]; + @state() private _updatesCount = 0; + @state() private _renderEmptySortable = false; private _mouseLeaveTimeout?: number; @@ -235,6 +238,7 @@ class HaSidebar extends LitElement { changedProps.has("narrow") || changedProps.has("alwaysExpand") || changedProps.has("_externalConfig") || + changedProps.has("_updatesCount") || changedProps.has("_notifications") || changedProps.has("editMode") || changedProps.has("_renderEmptySortable") || @@ -290,6 +294,12 @@ class HaSidebar extends LitElement { toggleAttribute(this, "rtl", computeRTL(this.hass)); } + this._updatesCount = Object.values(this.hass.states).filter( + (entity) => + computeStateDomain(entity) === "update" && + updateCanInstall(entity as UpdateEntity) + ).length; + if (!SUPPORT_SCROLL_IF_NEEDED) { return; } @@ -387,35 +397,37 @@ class HaSidebar extends LitElement { icon?: string | null, iconPath?: string | null ) { - return html` - - - ${iconPath - ? html`` - : html``} - ${title} - - ${this.editMode - ? html`` - : ""} - - `; + return urlPath === "config" + ? this._renderConfiguration(title) + : html` + + + ${iconPath + ? html`` + : html``} + ${title} + + ${this.editMode + ? html`` + : ""} + + `; } private _renderPanelsEdit(beforeSpacer: PanelInfo[]) { @@ -477,6 +489,35 @@ class HaSidebar extends LitElement { return html`
`; } + private _renderConfiguration(title: string | null) { + return html` + + + ${!this.alwaysExpand && this._updatesCount > 0 + ? html` + + ${this._updatesCount} + + ` + : ""} + ${title} + ${this.alwaysExpand && this._updatesCount > 0 + ? html` + ${this._updatesCount} + ` + : ""} + + `; + } + private _renderNotifications() { let notificationCount = this._notifications ? this._notifications.length @@ -953,18 +994,21 @@ class HaSidebar extends LitElement { height: 1px; background-color: var(--divider-color); } - .notifications-container { + .notifications-container, + .configuration-container { display: flex; margin-left: env(safe-area-inset-left); } - :host([rtl]) .notifications-container { + :host([rtl]) .notifications-container, + :host([rtl]) .configuration-container { margin-left: initial; margin-right: env(safe-area-inset-right); } .notifications { cursor: pointer; } - .notifications .item-text { + .notifications .item-text, + .configuration .item-text { flex: 1; } .profile { @@ -988,7 +1032,8 @@ class HaSidebar extends LitElement { margin-right: 8px; } - .notification-badge { + .notification-badge, + .configuration-badge { min-width: 20px; box-sizing: border-box; border-radius: 50%; @@ -999,7 +1044,11 @@ class HaSidebar extends LitElement { padding: 0px 6px; color: var(--text-accent-color, var(--text-primary-color)); } - ha-svg-icon + .notification-badge { + .configuration-badge { + background-color: var(--primary-color); + } + ha-svg-icon + .notification-badge, + ha-svg-icon + .configuration-badge { position: absolute; bottom: 14px; left: 26px; diff --git a/src/components/ha-tab.ts b/src/components/ha-tab.ts index da172e1fab..106ebd6211 100644 --- a/src/components/ha-tab.ts +++ b/src/components/ha-tab.ts @@ -42,9 +42,7 @@ export class HaTab extends LitElement { @keydown=${this._handleKeyDown} > ${this.narrow ? html`` : ""} - ${!this.narrow || this.active - ? html`${this.name}` - : ""} + ${this.name} ${this._shouldRenderRipple ? html`` : ""} `; diff --git a/src/components/ha-textarea.ts b/src/components/ha-textarea.ts index 2a3bf1a040..ce7c1539eb 100644 --- a/src/components/ha-textarea.ts +++ b/src/components/ha-textarea.ts @@ -19,13 +19,11 @@ export class HaTextArea extends TextAreaBase { textfieldStyles, textareaStyles, css` - :host([autogrow]) { - max-height: 200px; - } :host([autogrow]) .mdc-text-field { position: relative; min-height: 74px; min-width: 178px; + max-height: 200px; } :host([autogrow]) .mdc-text-field:after { content: attr(data-value); diff --git a/src/panels/lovelace/components/hui-theme-select-editor.ts b/src/components/ha-theme-picker.ts similarity index 68% rename from src/panels/lovelace/components/hui-theme-select-editor.ts rename to src/components/ha-theme-picker.ts index 659b5af4e9..af064958b5 100644 --- a/src/panels/lovelace/components/hui-theme-select-editor.ts +++ b/src/components/ha-theme-picker.ts @@ -2,29 +2,31 @@ import "@material/mwc-button"; import "@material/mwc-list/mwc-list-item"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; -import "../../../components/ha-select"; -import { HomeAssistant } from "../../../types"; +import { fireEvent } from "../common/dom/fire_event"; +import { stopPropagation } from "../common/dom/stop_propagation"; +import { HomeAssistant } from "../types"; +import "./ha-select"; -@customElement("hui-theme-select-editor") -export class HuiThemeSelectEditor extends LitElement { +@customElement("ha-theme-picker") +export class HaThemePicker extends LitElement { @property() public value?: string; @property() public label?: string; @property({ attribute: false }) public hass?: HomeAssistant; + @property({ type: Boolean, reflect: true }) public disabled = false; + + @property({ type: Boolean }) public required = false; + protected render(): TemplateResult { return html` ${this.hass!.localize( - "ui.panel.lovelace.editor.card.generic.no_theme" + "ui.components.theme_picker.no_theme" )} ${Object.keys(this.hass!.themes.themes) @@ -64,6 +66,6 @@ export class HuiThemeSelectEditor extends LitElement { declare global { interface HTMLElementTagNameMap { - "hui-theme-select-editor": HuiThemeSelectEditor; + "ha-theme-picker": HaThemePicker; } } diff --git a/src/components/ha-time-input.ts b/src/components/ha-time-input.ts index 5efd5a13f3..2bf5659841 100644 --- a/src/components/ha-time-input.ts +++ b/src/components/ha-time-input.ts @@ -2,8 +2,8 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { useAmPm } from "../common/datetime/use_am_pm"; import { fireEvent } from "../common/dom/fire_event"; -import "./ha-base-time-input"; import { FrontendLocaleData } from "../data/translation"; +import "./ha-base-time-input"; import type { TimeChangedEvent } from "./ha-base-time-input"; @customElement("ha-time-input") @@ -16,6 +16,8 @@ export class HaTimeInput extends LitElement { @property({ type: Boolean }) public disabled = false; + @property({ type: Boolean }) public required = false; + @property({ type: Boolean, attribute: "enable-second" }) public enableSecond = false; @@ -43,6 +45,7 @@ export class HaTimeInput extends LitElement { .disabled=${this.disabled} @value-changed=${this._timeChanged} .enableSecond=${this.enableSecond} + .required=${this.required} >
`; } diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts index d315e804ef..d4a30cc158 100644 --- a/src/components/ha-yaml-editor.ts +++ b/src/components/ha-yaml-editor.ts @@ -31,6 +31,10 @@ export class HaYamlEditor extends LitElement { @property() public label?: string; + @property({ type: Boolean }) public readOnly = false; + + @property({ type: Boolean }) public required = false; + @state() private _yaml = ""; public setValue(value): void { @@ -57,10 +61,11 @@ export class HaYamlEditor extends LitElement { return html``; } return html` - ${this.label ? html`

${this.label}

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

${this.label}${this.required ? "*" : ""}

` : ""} ; } export interface StateTrigger extends BaseTrigger { diff --git a/src/data/backup.ts b/src/data/backup.ts new file mode 100644 index 0000000000..129b76726d --- /dev/null +++ b/src/data/backup.ts @@ -0,0 +1,36 @@ +import { HomeAssistant } from "../types"; + +export interface BackupContent { + slug: string; + date: string; + name: string; + size: number; + path: string; +} + +export interface BackupData { + backing_up: boolean; + backups: BackupContent[]; +} + +export const getBackupDownloadUrl = (slug: string) => + `/api/backup/download/${slug}`; + +export const fetchBackupInfo = (hass: HomeAssistant): Promise => + hass.callWS({ + type: "backup/info", + }); + +export const removeBackup = ( + hass: HomeAssistant, + slug: string +): Promise => + hass.callWS({ + type: "backup/remove", + slug, + }); + +export const generateBackup = (hass: HomeAssistant): Promise => + hass.callWS({ + type: "backup/generate", + }); diff --git a/src/data/cached-history.ts b/src/data/cached-history.ts index dc7c9a9847..a75f9fd2f0 100644 --- a/src/data/cached-history.ts +++ b/src/data/cached-history.ts @@ -7,6 +7,7 @@ import { HistoryResult, LineChartUnit, TimelineEntity, + entityIdHistoryNeedsAttributes, } from "./history"; export interface CacheConfig { @@ -53,7 +54,17 @@ export const getRecent = ( return cache.data; } - const prom = fetchRecent(hass, entityId, startTime, endTime).then( + const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); + const prom = fetchRecent( + hass, + entityId, + startTime, + endTime, + false, + undefined, + true, + noAttributes + ).then( (stateHistory) => computeHistory(hass, stateHistory, localize), (err) => { delete RECENT_CACHE[entityId]; @@ -120,6 +131,7 @@ export const getRecentWithCache = ( } const curCacheProm = cache.prom; + const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId); const genProm = async () => { let fetchedHistory: HassEntity[][]; @@ -132,7 +144,10 @@ export const getRecentWithCache = ( entityId, toFetchStartTime, endTime, - appendingToCache + appendingToCache, + undefined, + true, + noAttributes ), ]); fetchedHistory = results[1]; diff --git a/src/data/camera.ts b/src/data/camera.ts index d556578ae2..48bbe4bc73 100644 --- a/src/data/camera.ts +++ b/src/data/camera.ts @@ -2,7 +2,7 @@ import { HassEntityAttributeBase, HassEntityBase, } from "home-assistant-js-websocket"; -import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise"; +import { timeCacheEntityPromiseFunc } from "../common/util/time-cache-entity-promise-func"; import { HomeAssistant } from "../types"; import { getSignedPath } from "./auth"; @@ -50,7 +50,7 @@ export const fetchThumbnailUrlWithCache = async ( width: number, height: number ) => { - const base_url = await timeCachePromiseFunc( + const base_url = await timeCacheEntityPromiseFunc( "_cameraTmbUrl", 9000, fetchThumbnailUrl, diff --git a/src/data/cloud.ts b/src/data/cloud.ts index e8add4f4ef..49a4244a1f 100644 --- a/src/data/cloud.ts +++ b/src/data/cloud.ts @@ -6,6 +6,7 @@ import { AutomationConfig } from "./automation"; interface CloudStatusNotLoggedIn { logged_in: false; cloud: "disconnected" | "connecting" | "connected"; + http_use_ssl: boolean; } export interface GoogleEntityConfig { @@ -59,6 +60,7 @@ export interface CloudStatusLoggedIn { remote_connected: boolean; remote_certificate: undefined | CertificateInformation; http_use_ssl: boolean; + active_subscription: boolean; } export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn; diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index f2e84ddac0..b9e555998c 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -34,8 +34,24 @@ export const ERROR_STATES: ConfigEntry["state"][] = [ "setup_retry", ]; -export const getConfigEntries = (hass: HomeAssistant) => - hass.callApi("GET", "config/config_entries/entry"); +export const getConfigEntries = ( + hass: HomeAssistant, + filters?: { type?: "helper" | "integration"; domain?: string } +): Promise => { + const params = new URLSearchParams(); + if (filters) { + if (filters.type) { + params.append("type", filters.type); + } + if (filters.domain) { + params.append("domain", filters.domain); + } + } + return hass.callApi( + "GET", + `config/config_entries/entry?${params.toString()}` + ); +}; export const updateConfigEntry = ( hass: HomeAssistant, diff --git a/src/data/config_flow.ts b/src/data/config_flow.ts index 39019393c5..c236a1b05c 100644 --- a/src/data/config_flow.ts +++ b/src/data/config_flow.ts @@ -65,8 +65,14 @@ export const ignoreConfigFlow = ( export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => hass.callApi("DELETE", `config/config_entries/flow/${flowId}`); -export const getConfigFlowHandlers = (hass: HomeAssistant) => - hass.callApi("GET", "config/config_entries/flow_handlers"); +export const getConfigFlowHandlers = ( + hass: HomeAssistant, + type?: "helper" | "integration" +) => + hass.callApi( + "GET", + `config/config_entries/flow_handlers${type ? `?type=${type}` : ""}` + ); export const fetchConfigFlowInProgress = ( conn: Connection diff --git a/src/data/data_entry_flow.ts b/src/data/data_entry_flow.ts index d617b37020..14821f8b77 100644 --- a/src/data/data_entry_flow.ts +++ b/src/data/data_entry_flow.ts @@ -28,7 +28,7 @@ export interface DataEntryFlowStepForm { step_id: string; data_schema: HaFormSchema[]; errors: Record; - description_placeholders: Record; + description_placeholders?: Record; last_step: boolean | null; } @@ -49,7 +49,7 @@ export interface DataEntryFlowStepCreateEntry { title: string; result?: ConfigEntry; description: string; - description_placeholders: Record; + description_placeholders?: Record; } export interface DataEntryFlowStepAbort { @@ -57,7 +57,7 @@ export interface DataEntryFlowStepAbort { flow_id: string; handler: string; reason: string; - description_placeholders: Record; + description_placeholders?: Record; } export interface DataEntryFlowStepProgress { @@ -66,7 +66,17 @@ export interface DataEntryFlowStepProgress { handler: string; step_id: string; progress_action: string; - description_placeholders: Record; + description_placeholders?: Record; +} + +export interface DataEntryFlowStepMenu { + type: "menu"; + flow_id: string; + handler: string; + step_id: string; + /** If array, use value to lookup translations in strings.json */ + menu_options: string[] | Record; + description_placeholders?: Record; } export type DataEntryFlowStep = @@ -74,7 +84,8 @@ export type DataEntryFlowStep = | DataEntryFlowStepExternal | DataEntryFlowStepCreateEntry | DataEntryFlowStepAbort - | DataEntryFlowStepProgress; + | DataEntryFlowStepProgress + | DataEntryFlowStepMenu; export const subscribeDataEntryFlowProgressed = ( conn: Connection, diff --git a/src/data/energy.ts b/src/data/energy.ts index d86419e9b2..d9f4f1c0c2 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -12,7 +12,12 @@ import { subscribeOne } from "../common/util/subscribe-one"; import { HomeAssistant } from "../types"; import { ConfigEntry, getConfigEntries } from "./config_entries"; import { subscribeEntityRegistry } from "./entity_registry"; -import { fetchStatistics, Statistics } from "./history"; +import { + fetchStatistics, + Statistics, + StatisticsMetaData, + getStatisticMetadata, +} from "./history"; const energyCollectionKeys: (string | undefined)[] = []; @@ -136,6 +141,7 @@ export interface GasSourceTypeEnergyPreference { entity_energy_from: string | null; entity_energy_price: string | null; number_energy_price: number | null; + unit_of_measurement?: string | null; } type EnergySource = @@ -241,14 +247,14 @@ const getEnergyData = async ( end?: Date ): Promise => { const [configEntries, entityRegistryEntries, info] = await Promise.all([ - getConfigEntries(hass), + getConfigEntries(hass, { domain: "co2signal" }), subscribeOne(hass.connection, subscribeEntityRegistry), getEnergyInfo(hass), ]); - const co2SignalConfigEntry = configEntries.find( - (entry) => entry.domain === "co2signal" - ); + const co2SignalConfigEntry = configEntries.length + ? configEntries[0] + : undefined; let co2SignalEntity: string | undefined; @@ -271,6 +277,15 @@ const getEnergyData = async ( const consumptionStatIDs: string[] = []; const statIDs: string[] = []; + const gasSources: GasSourceTypeEnergyPreference[] = + prefs.energy_sources.filter( + (source) => source.type === "gas" + ) as GasSourceTypeEnergyPreference[]; + const gasStatisticIdsWithMeta: StatisticsMetaData[] = + await getStatisticMetadata( + hass, + gasSources.map((source) => source.stat_energy_from) + ); for (const source of prefs.energy_sources) { if (source.type === "solar") { @@ -280,6 +295,20 @@ const getEnergyData = async ( if (source.type === "gas") { statIDs.push(source.stat_energy_from); + const entity = hass.states[source.stat_energy_from]; + if (!entity) { + for (const statisticIdWithMeta of gasStatisticIdsWithMeta) { + if ( + statisticIdWithMeta?.statistic_id === source.stat_energy_from && + statisticIdWithMeta?.unit_of_measurement + ) { + source.unit_of_measurement = + statisticIdWithMeta?.unit_of_measurement === "Wh" + ? "kWh" + : statisticIdWithMeta?.unit_of_measurement; + } + } + } if (source.stat_cost) { statIDs.push(source.stat_cost); } @@ -559,6 +588,9 @@ export const getEnergyGasUnit = ( ? "kWh" : entity.attributes.unit_of_measurement; } + if (source.unit_of_measurement) { + return source.unit_of_measurement; + } } return undefined; }; diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 83fcc601b5..e77d75c2b5 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -14,6 +14,7 @@ export interface EntityRegistryEntry { device_id: string | null; area_id: string | null; disabled_by: string | null; + hidden_by: string | null; entity_category: "config" | "diagnostic" | null; } @@ -38,7 +39,12 @@ export interface EntityRegistryEntryUpdateParams { device_class?: string | null; area_id?: string | null; disabled_by?: string | null; + hidden_by: string | null; new_entity_id?: string; + options_domain?: string; + options?: { + unit_of_measurement?: string | null; + }; } export const findBatteryEntity = ( diff --git a/src/data/entity_sources.ts b/src/data/entity_sources.ts new file mode 100644 index 0000000000..67455c1d22 --- /dev/null +++ b/src/data/entity_sources.ts @@ -0,0 +1,46 @@ +import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise"; +import { HomeAssistant } from "../types"; + +interface EntitySourceConfigEntry { + source: "config_entry"; + domain: string; + custom_component: boolean; + config_entry: string; +} + +interface EntitySourcePlatformConfig { + source: "platform_config"; + domain: string; + custom_component: boolean; +} + +export type EntitySources = Record< + string, + EntitySourceConfigEntry | EntitySourcePlatformConfig +>; + +const fetchEntitySources = ( + hass: HomeAssistant, + entity_id?: string +): Promise => + hass.callWS({ + type: "entity/source", + entity_id, + }); + +export const fetchEntitySourcesWithCache = ( + hass: HomeAssistant, + entity_id?: string +): Promise => + entity_id + ? fetchEntitySources(hass, entity_id) + : timeCachePromiseFunc( + "_entitySources", + // cache for 30 seconds + 30000, + fetchEntitySources, + // We base the cache on number of states. If number of states + // changes we force a refresh + (hass2) => Object.keys(hass2.states).length, + hass + ); diff --git a/src/data/helpers_crud.ts b/src/data/helpers_crud.ts new file mode 100644 index 0000000000..c03ce664ac --- /dev/null +++ b/src/data/helpers_crud.ts @@ -0,0 +1,71 @@ +import { fetchCounter, updateCounter, deleteCounter } from "./counter"; +import { + fetchInputBoolean, + updateInputBoolean, + deleteInputBoolean, +} from "./input_boolean"; +import { + fetchInputButton, + updateInputButton, + deleteInputButton, +} from "./input_button"; +import { + fetchInputDateTime, + updateInputDateTime, + deleteInputDateTime, +} from "./input_datetime"; +import { + fetchInputNumber, + updateInputNumber, + deleteInputNumber, +} from "./input_number"; +import { + fetchInputSelect, + updateInputSelect, + deleteInputSelect, +} from "./input_select"; +import { fetchInputText, updateInputText, deleteInputText } from "./input_text"; +import { fetchTimer, updateTimer, deleteTimer } from "./timer"; + +export const HELPERS_CRUD = { + input_boolean: { + fetch: fetchInputBoolean, + update: updateInputBoolean, + delete: deleteInputBoolean, + }, + input_button: { + fetch: fetchInputButton, + update: updateInputButton, + delete: deleteInputButton, + }, + input_text: { + fetch: fetchInputText, + update: updateInputText, + delete: deleteInputText, + }, + input_number: { + fetch: fetchInputNumber, + update: updateInputNumber, + delete: deleteInputNumber, + }, + input_datetime: { + fetch: fetchInputDateTime, + update: updateInputDateTime, + delete: deleteInputDateTime, + }, + input_select: { + fetch: fetchInputSelect, + update: updateInputSelect, + delete: deleteInputSelect, + }, + counter: { + fetch: fetchCounter, + update: updateCounter, + delete: deleteCounter, + }, + timer: { + fetch: fetchTimer, + update: updateTimer, + delete: deleteTimer, + }, +}; diff --git a/src/data/history.ts b/src/data/history.ts index 368dbcd3dc..a631c3423a 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -1,4 +1,5 @@ import { HassEntity } from "home-assistant-js-websocket"; +import { computeDomain } from "../common/entity/compute_domain"; import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateName } from "../common/entity/compute_state_name"; @@ -7,6 +8,13 @@ import { HomeAssistant } from "../types"; import { FrontendLocaleData } from "./translation"; const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"]; +const NEED_ATTRIBUTE_DOMAINS = [ + "climate", + "humidifier", + "input_datetime", + "thermostat", + "water_heater", +]; const LINE_ATTRIBUTES_TO_KEEP = [ "temperature", "current_temperature", @@ -76,6 +84,8 @@ export interface StatisticsMetaData { statistic_id: string; source: string; name?: string | null; + has_sum: boolean; + has_mean: boolean; } export type StatisticsValidationResult = @@ -131,6 +141,13 @@ export interface StatisticsValidationResults { [statisticId: string]: StatisticsValidationResult[]; } +export const entityIdHistoryNeedsAttributes = ( + hass: HomeAssistant, + entityId: string +) => + !hass.states[entityId] || + NEED_ATTRIBUTE_DOMAINS.includes(computeDomain(entityId)); + export const fetchRecent = ( hass: HomeAssistant, entityId: string, @@ -138,7 +155,8 @@ export const fetchRecent = ( endTime: Date, skipInitialState = false, significantChangesOnly?: boolean, - minimalResponse = true + minimalResponse = true, + noAttributes?: boolean ): Promise => { let url = "history/period"; if (startTime) { @@ -157,7 +175,9 @@ export const fetchRecent = ( if (minimalResponse) { url += "&minimal_response"; } - + if (noAttributes) { + url += "&no_attributes"; + } return hass.callApi("GET", url); }; @@ -171,6 +191,10 @@ export const fetchDate = ( "GET", `history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${ entityId ? `&filter_entity_id=${entityId}` : `` + }${ + entityId && !entityIdHistoryNeedsAttributes(hass, entityId) + ? `&no_attributes` + : `` }` ); @@ -278,6 +302,10 @@ const processLineChartEntities = ( }; }; +const stateUsesUnits = (state: HassEntity) => + "unit_of_measurement" in state.attributes || + "state_class" in state.attributes; + export const computeHistory = ( hass: HomeAssistant, stateHistory: HassEntity[][], @@ -294,16 +322,18 @@ export const computeHistory = ( return; } - const stateWithUnitorStateClass = stateInfo.find( - (state) => - state.attributes && - ("unit_of_measurement" in state.attributes || - "state_class" in state.attributes) - ); + const entityId = stateInfo[0].entity_id; + const currentState = + entityId in hass.states ? hass.states[entityId] : undefined; + const stateWithUnitorStateClass = + !currentState && + stateInfo.find((state) => state.attributes && stateUsesUnits(state)); let unit: string | undefined; - if (stateWithUnitorStateClass) { + if (currentState && stateUsesUnits(currentState)) { + unit = currentState.attributes.unit_of_measurement || " "; + } else if (stateWithUnitorStateClass) { unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " "; } else { unit = { @@ -313,7 +343,7 @@ export const computeHistory = ( input_number: "#", number: "#", water_heater: hass.config.unit_system.temperature, - }[computeStateDomain(stateInfo[0])]; + }[computeDomain(entityId)]; } if (!unit) { @@ -345,6 +375,15 @@ export const getStatisticIds = ( statistic_type, }); +export const getStatisticMetadata = ( + hass: HomeAssistant, + statistic_ids?: string[] +) => + hass.callWS({ + type: "recorder/get_statistics_metadata", + statistic_ids, + }); + export const fetchStatistics = ( hass: HomeAssistant, startTime: Date, @@ -428,3 +467,16 @@ export const statisticsHaveType = ( stats: StatisticValue[], type: StatisticType ) => stats.some((stat) => stat[type] !== null); + +export const adjustStatisticsSum = ( + hass: HomeAssistant, + statistic_id: string, + start_time: string, + adjustment: number +): Promise => + hass.callWS({ + type: "recorder/adjust_sum_statistics", + statistic_id, + start_time, + adjustment, + }); diff --git a/src/data/media-player.ts b/src/data/media-player.ts index 14b1a19c80..199cbbb386 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -16,6 +16,11 @@ import { mdiPlayPause, mdiPodcast, mdiPower, + mdiRepeat, + mdiRepeatOff, + mdiRepeatOnce, + mdiShuffle, + mdiShuffleDisabled, mdiSkipNext, mdiSkipPrevious, mdiStop, @@ -49,6 +54,8 @@ interface MediaPlayerEntityAttributes extends HassEntityAttributeBase { entity_picture_local?: string; is_volume_muted?: boolean; volume_level?: number; + repeat?: string; + shuffle?: boolean; source?: string; source_list?: string[]; sound_mode?: string; @@ -80,7 +87,9 @@ export const SUPPORT_VOLUME_BUTTONS = 1024; export const SUPPORT_SELECT_SOURCE = 2048; export const SUPPORT_STOP = 4096; export const SUPPORT_PLAY = 16384; +export const SUPPORT_REPEAT_SET = 262144; export const SUPPORT_SELECT_SOUND_MODE = 65536; +export const SUPPORT_SHUFFLE_SET = 32768; export const SUPPORT_BROWSE_MEDIA = 131072; export type MediaPlayerBrowseAction = "pick" | "play"; @@ -233,7 +242,8 @@ export const computeMediaDescription = ( }; export const computeMediaControls = ( - stateObj: MediaPlayerEntity + stateObj: MediaPlayerEntity, + useExtendedControls = false ): ControlButton[] | undefined => { if (!stateObj) { return undefined; @@ -266,6 +276,18 @@ export const computeMediaControls = ( } const assumedState = stateObj.attributes.assumed_state === true; + const stateAttr = stateObj.attributes; + + if ( + (state === "playing" || state === "paused" || assumedState) && + supportsFeature(stateObj, SUPPORT_SHUFFLE_SET) && + useExtendedControls + ) { + buttons.push({ + icon: stateAttr.shuffle === true ? mdiShuffle : mdiShuffleDisabled, + action: "shuffle_set", + }); + } if ( (state === "playing" || state === "paused" || assumedState) && @@ -337,6 +359,22 @@ export const computeMediaControls = ( }); } + if ( + (state === "playing" || state === "paused" || assumedState) && + supportsFeature(stateObj, SUPPORT_REPEAT_SET) && + useExtendedControls + ) { + buttons.push({ + icon: + stateAttr.repeat === "all" + ? mdiRepeat + : stateAttr.repeat === "one" + ? mdiRepeatOnce + : mdiRepeatOff, + action: "repeat_set", + }); + } + return buttons.length > 0 ? buttons : undefined; }; @@ -375,3 +413,31 @@ export const setMediaPlayerVolume = ( volume_level: number ) => hass.callService("media_player", "volume_set", { entity_id, volume_level }); + +export const handleMediaControlClick = ( + hass: HomeAssistant, + stateObj: MediaPlayerEntity, + action: string +) => + hass!.callService( + "media_player", + action, + action === "shuffle_set" + ? { + entity_id: stateObj!.entity_id, + shuffle: !stateObj!.attributes.shuffle, + } + : action === "repeat_set" + ? { + entity_id: stateObj!.entity_id, + repeat: + stateObj!.attributes.repeat === "all" + ? "one" + : stateObj!.attributes.repeat === "off" + ? "all" + : "off", + } + : { + entity_id: stateObj!.entity_id, + } + ); diff --git a/src/data/ozw.ts b/src/data/ozw.ts deleted file mode 100644 index 011a685fd8..0000000000 --- a/src/data/ozw.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { HomeAssistant } from "../types"; -import { DeviceRegistryEntry } from "./device_registry"; - -export interface OZWNodeIdentifiers { - ozw_instance: number; - node_id: number; -} - -export interface OZWDevice { - node_id: number; - node_query_stage: string; - is_awake: boolean; - is_failed: boolean; - is_zwave_plus: boolean; - ozw_instance: number; - event: string; - node_manufacturer_name: string; - node_product_name: string; -} - -export interface OZWDeviceMetaDataResponse { - node_id: number; - ozw_instance: number; - metadata: OZWDeviceMetaData; -} - -export interface OZWDeviceMetaData { - OZWInfoURL: string; - ZWAProductURL: string; - ProductPic: string; - Description: string; - ProductManualURL: string; - ProductPageURL: string; - InclusionHelp: string; - ExclusionHelp: string; - ResetHelp: string; - WakeupHelp: string; - ProductSupportURL: string; - Frequency: string; - Name: string; - ProductPicBase64: string; -} - -export interface OZWInstance { - ozw_instance: number; - OZWDaemon_Version: string; - OpenZWave_Version: string; - QTOpenZWave_Version: string; - Status: string; - getControllerPath: string; - homeID: string; -} - -export interface OZWNetworkStatistics { - ozw_instance: number; - node_count: number; - readCnt: number; - writeCnt: number; - ACKCnt: number; - CANCnt: number; - NAKCnt: number; - dropped: number; - retries: number; -} - -export interface OZWDeviceConfig { - label: string; - type: string; - value: string | number; - parameter: number; - min: number; - max: number; - help: string; -} - -export const nodeQueryStages = [ - "ProtocolInfo", - "Probe", - "WakeUp", - "ManufacturerSpecific1", - "NodeInfo", - "NodePlusInfo", - "ManufacturerSpecific2", - "Versions", - "Instances", - "Static", - "CacheLoad", - "Associations", - "Neighbors", - "Session", - "Dynamic", - "Configuration", - "Complete", -]; - -export const networkOnlineStatuses = [ - "driverAllNodesQueried", - "driverAllNodesQueriedSomeDead", - "driverAwakeNodesQueried", -]; -export const networkStartingStatuses = [ - "starting", - "started", - "Ready", - "driverReady", -]; -export const networkOfflineStatuses = [ - "Offline", - "stopped", - "driverFailed", - "driverReset", - "driverRemoved", - "driverAllNodesOnFire", -]; - -export const getIdentifiersFromDevice = function ( - device: DeviceRegistryEntry -): OZWNodeIdentifiers | undefined { - if (!device) { - return undefined; - } - - const ozwIdentifier = device.identifiers.find( - (identifier) => identifier[0] === "ozw" - ); - if (!ozwIdentifier) { - return undefined; - } - - const identifiers = ozwIdentifier[1].split("."); - return { - node_id: parseInt(identifiers[1]), - ozw_instance: parseInt(identifiers[0]), - }; -}; - -export const fetchOZWInstances = ( - hass: HomeAssistant -): Promise => - hass.callWS({ - type: "ozw/get_instances", - }); - -export const fetchOZWNetworkStatus = ( - hass: HomeAssistant, - ozw_instance: number -): Promise => - hass.callWS({ - type: "ozw/network_status", - ozw_instance, - }); - -export const fetchOZWNetworkStatistics = ( - hass: HomeAssistant, - ozw_instance: number -): Promise => - hass.callWS({ - type: "ozw/network_statistics", - ozw_instance, - }); - -export const fetchOZWNodes = ( - hass: HomeAssistant, - ozw_instance: number -): Promise => - hass.callWS({ - type: "ozw/get_nodes", - ozw_instance, - }); - -export const fetchOZWNodeStatus = ( - hass: HomeAssistant, - ozw_instance: number, - node_id: number -): Promise => - hass.callWS({ - type: "ozw/node_status", - ozw_instance, - node_id, - }); - -export const fetchOZWNodeMetadata = ( - hass: HomeAssistant, - ozw_instance: number, - node_id: number -): Promise => - hass.callWS({ - type: "ozw/node_metadata", - ozw_instance, - node_id, - }); - -export const fetchOZWNodeConfig = ( - hass: HomeAssistant, - ozw_instance: number, - node_id: number -): Promise => - hass.callWS({ - type: "ozw/get_config_parameters", - ozw_instance, - node_id, - }); - -export const refreshNodeInfo = ( - hass: HomeAssistant, - ozw_instance: number, - node_id: number -): Promise => - hass.callWS({ - type: "ozw/refresh_node_info", - ozw_instance, - node_id, - }); diff --git a/src/data/selector.ts b/src/data/selector.ts index c8cc87b36e..10cda864b7 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -1,52 +1,30 @@ export type Selector = + | ActionSelector | AddonSelector + | AreaSelector | AttributeSelector - | EntitySelector + | BooleanSelector + | ColorRGBSelector + | ColorTempSelector + | DateSelector + | DateTimeSelector | DeviceSelector | DurationSelector - | AreaSelector - | TargetSelector + | EntitySelector + | IconSelector + | LocationSelector + | MediaSelector | NumberSelector - | BooleanSelector - | TimeSelector - | ActionSelector - | StringSelector | ObjectSelector | SelectSelector - | IconSelector - | MediaSelector + | StringSelector + | TargetSelector | ThemeSelector - | LocationSelector; + | TimeSelector; -export interface EntitySelector { - entity: { - integration?: string; - domain?: string | string[]; - device_class?: string; - }; -} - -export interface AttributeSelector { - attribute: { - entity_id: string; - }; -} - -export interface DeviceSelector { - device: { - integration?: string; - manufacturer?: string; - model?: string; - entity?: { - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; - }; -} - -export interface DurationSelector { +export interface ActionSelector { // eslint-disable-next-line @typescript-eslint/ban-types - duration: {}; + action: {}; } export interface AddonSelector { @@ -68,21 +46,105 @@ export interface AreaSelector { manufacturer?: DeviceSelector["device"]["manufacturer"]; model?: DeviceSelector["device"]["model"]; }; + multiple?: boolean; }; } -export interface TargetSelector { - target: { +export interface AttributeSelector { + attribute: { + entity_id?: string; + }; +} + +export interface BooleanSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + boolean: {}; +} + +export interface ColorRGBSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + color_rgb: {}; +} + +export interface ColorTempSelector { + color_temp: { + min_mireds?: number; + max_mireds?: number; + }; +} + +export interface DateSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + date: {}; +} + +export interface DateTimeSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + datetime: {}; +} + +export interface DeviceSelector { + device: { + integration?: string; + manufacturer?: string; + model?: string; 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"]; - }; + multiple?: boolean; + }; +} + +export interface DurationSelector { + duration: { + enable_day?: boolean; + }; +} + +export interface EntitySelector { + entity: { + integration?: string; + domain?: string | string[]; + device_class?: string; + multiple?: boolean; + include_entities?: string[]; + exclude_entities?: string[]; + }; +} + +export interface IconSelector { + icon: { + placeholder?: string; + fallbackPath?: string; + }; +} + +export interface LocationSelector { + location: { radius?: boolean; icon?: string }; +} + +export interface LocationSelectorValue { + latitude: number; + longitude: number; + radius?: number; +} + +export interface MediaSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + media: {}; +} + +export interface MediaSelectorValue { + entity_id?: string; + media_content_id?: string; + media_content_type?: string; + metadata?: { + title?: string; + thumbnail?: string | null; + media_class?: string; + children_media_class?: string | null; + navigateIds?: { media_content_type: string; media_content_id: string }[]; }; } @@ -96,19 +158,23 @@ export interface NumberSelector { }; } -export interface BooleanSelector { +export interface ObjectSelector { // eslint-disable-next-line @typescript-eslint/ban-types - boolean: {}; + object: {}; } -export interface TimeSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - time: {}; +export interface SelectOption { + value: string; + label: string; } -export interface ActionSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - action: {}; +export interface SelectSelector { + select: { + multiple?: boolean; + custom_value?: boolean; + mode?: "list" | "dropdown"; + options: string[] | SelectOption[]; + }; } export interface StringSelector { @@ -132,26 +198,18 @@ export interface StringSelector { }; } -export interface ObjectSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - object: {}; -} - -export interface SelectOption { - value: string; - label: string; -} - -export interface SelectSelector { - select: { - options: string[] | SelectOption[]; - }; -} - -export interface IconSelector { - icon: { - placeholder?: string; - fallbackPath?: string; +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"]; + }; }; } @@ -159,31 +217,7 @@ export interface ThemeSelector { // eslint-disable-next-line @typescript-eslint/ban-types theme: {}; } - -export interface MediaSelector { +export interface TimeSelector { // eslint-disable-next-line @typescript-eslint/ban-types - media: {}; -} - -export interface LocationSelector { - location: { radius?: boolean; icon?: string }; -} - -export interface LocationSelectorValue { - latitude: number; - longitude: number; - radius?: number; -} - -export interface MediaSelectorValue { - entity_id?: string; - media_content_id?: string; - media_content_type?: string; - metadata?: { - title?: string; - thumbnail?: string | null; - media_class?: string; - children_media_class?: string | null; - navigateIds?: { media_content_type: string; media_content_id: string }[]; - }; + time: {}; } diff --git a/src/data/supervisor/root.ts b/src/data/supervisor/root.ts deleted file mode 100644 index 51fe449ecd..0000000000 --- a/src/data/supervisor/root.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { HomeAssistant } from "../../types"; - -interface SupervisorBaseAvailableUpdates { - panel_path?: string; - update_type?: string; - version_latest?: string; -} - -interface SupervisorAddonAvailableUpdates - extends SupervisorBaseAvailableUpdates { - update_type?: "addon"; - icon?: string; - name?: string; -} - -interface SupervisorCoreAvailableUpdates - extends SupervisorBaseAvailableUpdates { - update_type?: "core"; -} - -interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates { - update_type?: "os"; -} - -interface SupervisorSupervisorAvailableUpdates - extends SupervisorBaseAvailableUpdates { - update_type?: "supervisor"; -} - -export type SupervisorAvailableUpdates = - | SupervisorAddonAvailableUpdates - | SupervisorCoreAvailableUpdates - | SupervisorOsAvailableUpdates - | SupervisorSupervisorAvailableUpdates; - -export interface SupervisorAvailableUpdatesResponse { - available_updates: SupervisorAvailableUpdates[]; -} - -export const fetchSupervisorAvailableUpdates = async ( - hass: HomeAssistant -): Promise => - ( - await hass.callWS({ - type: "supervisor/api", - endpoint: "/available_updates", - method: "get", - }) - ).available_updates; - -export const refreshSupervisorAvailableUpdates = async ( - hass: HomeAssistant -): Promise => - hass.callWS({ - type: "supervisor/api", - endpoint: "/refresh_updates", - method: "post", - }); diff --git a/src/data/timer.ts b/src/data/timer.ts index cff601b60a..4631454e12 100644 --- a/src/data/timer.ts +++ b/src/data/timer.ts @@ -12,6 +12,7 @@ export type TimerEntity = HassEntityBase & { attributes: HassEntityAttributeBase & { duration: string; remaining: string; + restore: boolean; }; }; @@ -26,12 +27,14 @@ export interface Timer { name: string; icon?: string; duration?: string | number | DurationDict; + restore?: boolean; } export interface TimerMutableParams { name: string; icon: string; duration: string | number | DurationDict; + restore: boolean; } export const fetchTimer = (hass: HomeAssistant) => diff --git a/src/data/update.ts b/src/data/update.ts new file mode 100644 index 0000000000..7482412343 --- /dev/null +++ b/src/data/update.ts @@ -0,0 +1,44 @@ +import type { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; +import { supportsFeature } from "../common/entity/supports-feature"; +import { HomeAssistant } from "../types"; + +export const UPDATE_SUPPORT_INSTALL = 1; +export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2; +export const UPDATE_SUPPORT_PROGRESS = 4; +export const UPDATE_SUPPORT_BACKUP = 8; +export const UPDATE_SUPPORT_RELEASE_NOTES = 16; + +interface UpdateEntityAttributes extends HassEntityAttributeBase { + current_version: string | null; + in_progress: boolean | number; + latest_version: string | null; + release_summary: string | null; + release_url: string | null; + skipped_version: string | null; + title: string | null; +} + +export interface UpdateEntity extends HassEntityBase { + attributes: UpdateEntityAttributes; +} + +export const updateUsesProgress = (entity: UpdateEntity): boolean => + supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) && + typeof entity.attributes.in_progress === "number"; + +export const updateCanInstall = (entity: UpdateEntity): boolean => + supportsFeature(entity, UPDATE_SUPPORT_INSTALL) && + entity.attributes.latest_version !== entity.attributes.current_version && + entity.attributes.latest_version !== entity.attributes.skipped_version; + +export const updateIsInstalling = (entity: UpdateEntity): boolean => + updateUsesProgress(entity) || !!entity.attributes.in_progress; + +export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) => + hass.callWS({ + type: "update/release_notes", + entity_id: entityId, + }); diff --git a/src/data/zwave.ts b/src/data/zwave.ts deleted file mode 100644 index fb6fbe935e..0000000000 --- a/src/data/zwave.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { HomeAssistant } from "../types"; - -export interface ZWaveNetworkStatus { - state: number; -} - -export interface ZWaveValue { - key: number; - value: { - index: number; - instance: number; - label: string; - poll_intensity: number; - }; -} - -export interface ZWaveConfigItem { - key: number; - value: { - data: any; - data_items: any[]; - help: string; - label: string; - max: number; - min: number; - type: string; - }; -} - -export interface ZWaveConfigServiceData { - node_id: number; - parameter: number; - value: number | string; -} - -export interface ZWaveNode { - attributes: ZWaveAttributes; -} - -export interface ZWaveAttributes { - node_id: number; - wake_up_interval?: number; -} - -export interface ZWaveMigrationConfig { - usb_path: string; - network_key: string; -} - -export const ZWAVE_NETWORK_STATE_STOPPED = 0; -export const ZWAVE_NETWORK_STATE_FAILED = 1; -export const ZWAVE_NETWORK_STATE_STARTED = 5; -export const ZWAVE_NETWORK_STATE_AWAKED = 7; -export const ZWAVE_NETWORK_STATE_READY = 10; - -export const fetchNetworkStatus = ( - hass: HomeAssistant -): Promise => - hass.callWS({ - type: "zwave/network_status", - }); - -export const startZwaveJsConfigFlow = ( - hass: HomeAssistant -): Promise<{ flow_id: string }> => - hass.callWS({ - type: "zwave/start_zwave_js_config_flow", - }); - -export const fetchMigrationConfig = ( - hass: HomeAssistant -): Promise => - hass.callWS({ - type: "zwave/get_migration_config", - }); - -export const fetchValues = (hass: HomeAssistant, nodeId: number) => - hass.callApi("GET", `zwave/values/${nodeId}`); - -export const fetchNodeConfig = (hass: HomeAssistant, nodeId: number) => - hass.callApi("GET", `zwave/config/${nodeId}`); diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 6fbab75c74..1d84b6efed 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -1,5 +1,5 @@ import "@material/mwc-button"; -import { mdiClose } from "@mdi/js"; +import { mdiClose, mdiHelpCircle } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -14,9 +14,7 @@ import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event"; import { computeRTL } from "../../common/util/compute_rtl"; import "../../components/ha-circular-progress"; import "../../components/ha-dialog"; -import "../../components/ha-form/ha-form"; import "../../components/ha-icon-button"; -import "../../components/ha-markdown"; import { AreaRegistryEntry, subscribeAreaRegistry, @@ -33,9 +31,11 @@ import { } from "../../data/device_registry"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; +import { documentationUrl } from "../../util/documentation-url"; import { showAlertDialog } from "../generic/show-dialog-box"; import { DataEntryFlowDialogParams, + FlowHandlers, LoadingReason, } from "./show-dialog-data-entry-flow"; import "./step-flow-abort"; @@ -46,6 +46,7 @@ import "./step-flow-loading"; import "./step-flow-pick-flow"; import "./step-flow-pick-handler"; import "./step-flow-progress"; +import "./step-flow-menu"; let instance = 0; @@ -85,7 +86,7 @@ class DataEntryFlowDialog extends LitElement { @state() private _areas?: AreaRegistryEntry[]; - @state() private _handlers?: string[]; + @state() private _handlers?: FlowHandlers; @state() private _handler?: string; @@ -235,14 +236,35 @@ class DataEntryFlowDialog extends LitElement { // to reset the element. "" : html` - +
+ ${["form", "menu", "external"].includes( + this._step?.type as any + ) + ? html` + + ` + : ""} + +
${this._step === null ? this._handler ? html` ` + : this._step.type === "menu" + ? html` + + ` : this._devices === undefined || this._areas === undefined ? // When it's a create entry result, we will fetch device & area registry html` @@ -421,7 +451,7 @@ class DataEntryFlowDialog extends LitElement { title: this.hass.localize( "ui.panel.config.integrations.config_flow.error" ), - text: err.message || err.body, + text: err?.body?.message, }); return; } finally { @@ -463,16 +493,19 @@ class DataEntryFlowDialog extends LitElement { ha-dialog { --dialog-content-padding: 0; } - ha-icon-button { + .dialog-actions { padding: 16px; position: absolute; top: 0; right: 0; } - ha-icon-button[rtl] { + .dialog-actions[rtl] { right: auto; left: 0; } + .dialog-actions > * { + color: var(--secondary-text-color); + } `, ]; } diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index e516dfc4fe..53c92fd214 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -1,5 +1,4 @@ import { html } from "lit"; -import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { createConfigFlow, deleteConfigFlow, @@ -23,17 +22,12 @@ export const showConfigFlowDialog = ( showFlowDialog(element, dialogParams, { loadDevicesAndAreas: true, getFlowHandlers: async (hass) => { - const [handlers] = await Promise.all([ - getConfigFlowHandlers(hass), + const [integrations, helpers] = await Promise.all([ + getConfigFlowHandlers(hass, "integration"), + getConfigFlowHandlers(hass, "helper"), hass.loadBackendTranslation("title", undefined, true), ]); - - return handlers.sort((handlerA, handlerB) => - caseInsensitiveStringCompare( - domainToName(hass.localize, handlerA), - domainToName(hass.localize, handlerB) - ) - ); + return { integrations, helpers }; }, createFlow: async (hass, handler) => { const [step] = await Promise.all([ @@ -91,6 +85,12 @@ export const showConfigFlowDialog = ( ); }, + renderShowFormStepFieldHelper(hass, step, field) { + return hass.localize( + `component.${step.handler}.config.step.${step.step_id}.data_description.${field.name}` + ); + }, + renderShowFormStepFieldError(hass, step, error) { return hass.localize( `component.${step.handler}.config.error.${error}`, @@ -181,6 +181,33 @@ export const showConfigFlowDialog = ( : ""; }, + renderMenuHeader(hass, step) { + return ( + hass.localize( + `component.${step.handler}.config.step.${step.step_id}.title` + ) || hass.localize(`component.${step.handler}.title`) + ); + }, + + renderMenuDescription(hass, step) { + const description = hass.localize( + `component.${step.handler}.config.step.${step.step_id}.description`, + step.description_placeholders + ); + return description + ? html` + + ` + : ""; + }, + + renderMenuOption(hass, step, option) { + return hass.localize( + `component.${step.handler}.config.step.${step.step_id}.menu_options.${option}`, + step.description_placeholders + ); + }, + renderLoadingDescription(hass, reason, handler, step) { if (!["loading_flow", "loading_step"].includes(reason)) { return ""; 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 b0217ec057..35f6630216 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -7,14 +7,19 @@ import { DataEntryFlowStepCreateEntry, DataEntryFlowStepExternal, DataEntryFlowStepForm, + DataEntryFlowStepMenu, DataEntryFlowStepProgress, } from "../../data/data_entry_flow"; import { HomeAssistant } from "../../types"; +export interface FlowHandlers { + integrations: string[]; + helpers: string[]; +} export interface FlowConfig { loadDevicesAndAreas: boolean; - getFlowHandlers?: (hass: HomeAssistant) => Promise; + getFlowHandlers?: (hass: HomeAssistant) => Promise; createFlow(hass: HomeAssistant, handler: string): Promise; @@ -49,6 +54,12 @@ export interface FlowConfig { field: HaFormSchema ): string; + renderShowFormStepFieldHelper( + hass: HomeAssistant, + step: DataEntryFlowStepForm, + field: HaFormSchema + ): string; + renderShowFormStepFieldError( hass: HomeAssistant, step: DataEntryFlowStepForm, @@ -80,6 +91,19 @@ export interface FlowConfig { step: DataEntryFlowStepProgress ): TemplateResult | ""; + renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string; + + renderMenuDescription( + hass: HomeAssistant, + step: DataEntryFlowStepMenu + ): TemplateResult | ""; + + renderMenuOption( + hass: HomeAssistant, + step: DataEntryFlowStepMenu, + option: string + ): string; + renderLoadingDescription( hass: HomeAssistant, loadingReason: LoadingReason, diff --git a/src/dialogs/config-flow/show-dialog-options-flow.ts b/src/dialogs/config-flow/show-dialog-options-flow.ts index 7c7fde94dc..4a90b08889 100644 --- a/src/dialogs/config-flow/show-dialog-options-flow.ts +++ b/src/dialogs/config-flow/show-dialog-options-flow.ts @@ -89,6 +89,12 @@ export const showOptionsFlowDialog = ( ); }, + renderShowFormStepFieldHelper(hass, step, field) { + return hass.localize( + `component.${configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}` + ); + }, + renderShowFormStepFieldError(hass, step, error) { return hass.localize( `component.${configEntry.domain}.options.error.${error}`, @@ -134,6 +140,37 @@ export const showOptionsFlowDialog = ( : ""; }, + renderMenuHeader(hass, step) { + return ( + hass.localize( + `component.${step.handler}.option.step.${step.step_id}.title` + ) || hass.localize(`component.${step.handler}.title`) + ); + }, + + renderMenuDescription(hass, step) { + const description = hass.localize( + `component.${step.handler}.option.step.${step.step_id}.description`, + step.description_placeholders + ); + return description + ? html` + + ` + : ""; + }, + + renderMenuOption(hass, step, option) { + return hass.localize( + `component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`, + step.description_placeholders + ); + }, + renderLoadingDescription(hass, reason) { return ( hass.localize(`component.${configEntry.domain}.options.loading`) || diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index 59e3656b1b..e88128b326 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -54,6 +54,7 @@ class StepFlowForm extends LitElement { .schema=${step.data_schema} .error=${step.errors} .computeLabel=${this._labelCallback} + .computeHelper=${this._helperCallback} .computeError=${this._errorCallback} > @@ -166,6 +167,9 @@ class StepFlowForm extends LitElement { private _labelCallback = (field: HaFormSchema): string => this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field); + private _helperCallback = (field: HaFormSchema): string => + this.flowConfig.renderShowFormStepFieldHelper(this.hass, this.step, field); + private _errorCallback = (error: string) => this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error); diff --git a/src/dialogs/config-flow/step-flow-menu.ts b/src/dialogs/config-flow/step-flow-menu.ts new file mode 100644 index 0000000000..66d0b8d0c6 --- /dev/null +++ b/src/dialogs/config-flow/step-flow-menu.ts @@ -0,0 +1,96 @@ +import "@material/mwc-list/mwc-list-item"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import type { DataEntryFlowStepMenu } from "../../data/data_entry_flow"; +import type { HomeAssistant } from "../../types"; +import type { FlowConfig } from "./show-dialog-data-entry-flow"; +import "../../components/ha-icon-next"; +import { configFlowContentStyles } from "./styles"; +import { fireEvent } from "../../common/dom/fire_event"; + +@customElement("step-flow-menu") +class StepFlowMenu extends LitElement { + @property({ attribute: false }) public flowConfig!: FlowConfig; + + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public step!: DataEntryFlowStepMenu; + + protected render(): TemplateResult { + let options: string[]; + let translations: Record; + + if (Array.isArray(this.step.menu_options)) { + options = this.step.menu_options; + translations = {}; + for (const option of options) { + translations[option] = this.flowConfig.renderMenuOption( + this.hass, + this.step, + option + ); + } + } else { + options = Object.keys(this.step.menu_options); + translations = this.step.menu_options; + } + + const description = this.flowConfig.renderMenuDescription( + this.hass, + this.step + ); + + return html` +

${this.flowConfig.renderMenuHeader(this.hass, this.step)}

+ ${description ? html`
${description}
` : ""} +
+ ${options.map( + (option) => html` + + ${translations[option]} + + + ` + )} +
+ `; + } + + private _handleStep(ev) { + fireEvent(this, "flow-update", { + stepPromise: this.flowConfig.handleFlowStep( + this.hass, + this.step.flow_id, + { + next_step_id: ev.currentTarget.step, + } + ), + }); + } + + static styles = [ + configFlowContentStyles, + css` + .options { + margin-top: 20px; + margin-bottom: 8px; + } + .content { + padding-bottom: 16px; + border-bottom: 1px solid var(--divider-color); + } + .content + .options { + margin-top: 8px; + } + mwc-list-item { + --mdc-list-side-padding: 24px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "step-flow-menu": StepFlowMenu; + } +} diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 9f2a7e6740..e40158dd97 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -26,11 +26,13 @@ import { HomeAssistant } from "../../types"; import { brandsUrl } from "../../util/brands-url"; import { documentationUrl } from "../../util/documentation-url"; import { configFlowContentStyles } from "./styles"; +import { FlowHandlers } from "./show-dialog-data-entry-flow"; interface HandlerObj { name: string; slug: string; is_add?: boolean; + is_helper?: boolean; } declare global { @@ -46,7 +48,7 @@ declare global { class StepFlowPickHandler extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public handlers!: string[]; + @property({ attribute: false }) public handlers!: FlowHandlers; @property() public initialFilter?: string; @@ -57,8 +59,12 @@ class StepFlowPickHandler extends LitElement { private _height?: number; private _filterHandlers = memoizeOne( - (h: string[], filter?: string, _localize?: LocalizeFunc) => { - const handlers: HandlerObj[] = h.map((handler) => ({ + ( + h: FlowHandlers, + filter?: string, + _localize?: LocalizeFunc + ): [HandlerObj[], HandlerObj[]] => { + const integrations: HandlerObj[] = h.integrations.map((handler) => ({ name: domainToName(this.hass.localize, handler), slug: handler, })); @@ -70,17 +76,31 @@ class StepFlowPickHandler extends LitElement { minMatchCharLength: 2, threshold: 0.2, }; - const fuse = new Fuse(handlers, options); - return fuse.search(filter).map((result) => result.item); + const helpers: HandlerObj[] = h.helpers.map((handler) => ({ + name: domainToName(this.hass.localize, handler), + slug: handler, + is_helper: true, + })); + return [ + new Fuse(integrations, options) + .search(filter) + .map((result) => result.item), + new Fuse(helpers, options) + .search(filter) + .map((result) => result.item), + ]; } - return handlers.sort((a, b) => - caseInsensitiveStringCompare(a.name, b.name) - ); + return [ + integrations.sort((a, b) => + caseInsensitiveStringCompare(a.name, b.name) + ), + [], + ]; } ); protected render(): TemplateResult { - const handlers = this._getHandlers(); + const [integrations, helpers] = this._getHandlers(); const addDeviceRows: HandlerObj[] = ["zha", "zwave_js"] .filter((domain) => isComponentLoaded(this.hass, domain)) @@ -115,8 +135,8 @@ class StepFlowPickHandler extends LitElement { ` : ""} - ${handlers.length - ? handlers.map((handler) => this._renderRow(handler)) + ${integrations.length + ? integrations.map((handler) => this._renderRow(handler)) : html`

${this.hass.localize( @@ -139,6 +159,12 @@ class StepFlowPickHandler extends LitElement { >.

`} + ${helpers.length + ? html` + + ${helpers.map((handler) => this._renderRow(handler))} + ` + : ""} `; } @@ -162,7 +188,7 @@ class StepFlowPickHandler extends LitElement { })} referrerpolicy="no-referrer" /> - ${handler.name} + ${handler.name} ${handler.is_helper ? " (helper)" : ""} ${handler.is_add ? "" : html``} `; @@ -216,15 +242,16 @@ class StepFlowPickHandler extends LitElement { if (handler.is_add) { if (handler.slug === "zwave_js") { - const entries = await getConfigEntries(this.hass); - const entry = entries.find((ent) => ent.domain === "zwave_js"); + const entries = await getConfigEntries(this.hass, { + domain: "zwave_js", + }); - if (!entry) { + if (!entries.length) { return; } showZWaveJSAddNodeDialog(this, { - entry_id: entry.entry_id, + entry_id: entries[0].entry_id, }); } else if (handler.slug === "zha") { navigate("/config/zha/add"); @@ -235,6 +262,13 @@ class StepFlowPickHandler extends LitElement { return; } + if (handler.is_helper) { + navigate(`/config/helpers/add?domain=${handler.slug}`); + // This closes dialog. + fireEvent(this, "flow-update"); + return; + } + fireEvent(this, "handler-picked", { handler: handler.slug, }); @@ -249,7 +283,7 @@ class StepFlowPickHandler extends LitElement { if (handlers.length > 0) { fireEvent(this, "handler-picked", { - handler: handlers[0].slug, + handler: handlers[0][0].slug, }); } } diff --git a/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts index 1efb0094f7..de41f1c173 100644 --- a/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts +++ b/src/dialogs/more-info/controls/more-info-alarm_control_panel.ts @@ -12,7 +12,7 @@ import { import type { HomeAssistant } from "../../../types"; const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"]; -const ARM_ACTIONS = ["arm_away", "arm_home"]; +const ARM_ACTIONS = ["arm_home", "arm_away"]; const DISARM_ACTIONS = ["disarm"]; @customElement("more-info-alarm_control_panel") 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 916257695a..6f4b88926e 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.ts +++ b/src/dialogs/more-info/controls/more-info-media_player.ts @@ -23,6 +23,7 @@ import { showMediaBrowserDialog } from "../../../components/media-player/show-me import { UNAVAILABLE, UNKNOWN } from "../../../data/entity"; import { computeMediaControls, + handleMediaControlClick, MediaPickedEvent, MediaPlayerEntity, SUPPORT_BROWSE_MEDIA, @@ -47,7 +48,7 @@ class MoreInfoMediaPlayer extends LitElement { } const stateObj = this.stateObj; - const controls = computeMediaControls(stateObj); + const controls = computeMediaControls(stateObj, true); return html`
@@ -202,6 +203,7 @@ class MoreInfoMediaPlayer extends LitElement { } .basic-controls { + display: inline-flex; flex-grow: 1; } @@ -231,12 +233,10 @@ class MoreInfoMediaPlayer extends LitElement { } private _handleClick(e: MouseEvent): void { - this.hass!.callService( - "media_player", - (e.currentTarget! as HTMLElement).getAttribute("action")!, - { - entity_id: this.stateObj!.entity_id, - } + handleMediaControlClick( + this.hass!, + this.stateObj!, + (e.currentTarget as HTMLElement).getAttribute("action")! ); } diff --git a/src/dialogs/more-info/controls/more-info-timer.ts b/src/dialogs/more-info/controls/more-info-timer.ts index 55dbe3168a..63be3032f9 100644 --- a/src/dialogs/more-info/controls/more-info-timer.ts +++ b/src/dialogs/more-info/controls/more-info-timer.ts @@ -46,7 +46,7 @@ class MoreInfoTimer extends LitElement { `; } diff --git a/src/dialogs/more-info/controls/more-info-update.ts b/src/dialogs/more-info/controls/more-info-update.ts new file mode 100644 index 0000000000..44d3d84840 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-update.ts @@ -0,0 +1,248 @@ +import "../../../components/ha-alert"; +import "../../../components/ha-faded"; +import "@material/mwc-button/mwc-button"; +import "@material/mwc-linear-progress/mwc-linear-progress"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { supportsFeature } from "../../../common/entity/supports-feature"; +import "../../../components/ha-checkbox"; +import "../../../components/ha-circular-progress"; +import "../../../components/ha-formfield"; +import "../../../components/ha-markdown"; +import { UNAVAILABLE_STATES } from "../../../data/entity"; +import { + UpdateEntity, + updateIsInstalling, + updateReleaseNotes, + UPDATE_SUPPORT_BACKUP, + UPDATE_SUPPORT_INSTALL, + UPDATE_SUPPORT_PROGRESS, + UPDATE_SUPPORT_RELEASE_NOTES, + UPDATE_SUPPORT_SPECIFIC_VERSION, +} from "../../../data/update"; +import type { HomeAssistant } from "../../../types"; + +@customElement("more-info-update") +class MoreInfoUpdate extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj?: UpdateEntity; + + @state() private _releaseNotes?: string | null; + + @state() private _error?: string; + + protected render(): TemplateResult { + if ( + !this.hass || + !this.stateObj || + UNAVAILABLE_STATES.includes(this.stateObj.state) + ) { + return html``; + } + + const skippedVersion = + this.stateObj.attributes.latest_version && + this.stateObj.attributes.skipped_version === + this.stateObj.attributes.latest_version; + + return html` + ${this.stateObj.attributes.in_progress + ? supportsFeature(this.stateObj, UPDATE_SUPPORT_PROGRESS) && + typeof this.stateObj.attributes.in_progress === "number" + ? html`` + : html`` + : ""} + ${this.stateObj.attributes.title + ? html`

${this.stateObj.attributes.title}

` + : ""} + ${this._error + ? html`${this._error}` + : ""} + +
+
+ ${this.hass.localize( + "ui.dialogs.more_info_control.update.current_version" + )} +
+
+ ${this.stateObj.attributes.current_version ?? + this.hass.localize("state.default.unavailable")} +
+
+
+
+ ${this.hass.localize( + "ui.dialogs.more_info_control.update.latest_version" + )} +
+
+ ${this.stateObj.attributes.latest_version ?? + this.hass.localize("state.default.unavailable")} +
+
+ + ${this.stateObj.attributes.release_url + ? html`` + : ""} + ${supportsFeature(this.stateObj!, UPDATE_SUPPORT_RELEASE_NOTES) && + !this._error + ? this._releaseNotes === undefined + ? html`` + : html`
+ + + ` + : this.stateObj.attributes.release_summary + ? html`
+ ` + : ""} + ${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP) + ? html`
+ + + ` + : ""} +
+
+ + ${this.hass.localize("ui.dialogs.more_info_control.update.skip")} + + ${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL) + ? html` + + ${this.hass.localize( + "ui.dialogs.more_info_control.update.install" + )} + + ` + : ""} +
+ `; + } + + protected firstUpdated(): void { + if (supportsFeature(this.stateObj!, UPDATE_SUPPORT_RELEASE_NOTES)) { + updateReleaseNotes(this.hass, this.stateObj!.entity_id) + .then((result) => { + this._releaseNotes = result; + }) + .catch((err) => { + this._error = err.message; + }); + } + } + + get _shouldCreateBackup(): boolean | null { + if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) { + return null; + } + const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); + if (checkbox) { + return checkbox.checked; + } + return true; + } + + private _handleInstall(): void { + const installData: Record = { + entity_id: this.stateObj!.entity_id, + }; + + if (this._shouldCreateBackup) { + installData.backup = true; + } + + if ( + supportsFeature(this.stateObj!, UPDATE_SUPPORT_SPECIFIC_VERSION) && + this.stateObj!.attributes.latest_version + ) { + installData.version = this.stateObj!.attributes.latest_version; + } + + this.hass.callService("update", "install", installData); + } + + private _handleSkip(): void { + this.hass.callService("update", "skip", { + entity_id: this.stateObj!.entity_id, + }); + } + + static get styles(): CSSResultGroup { + return css` + hr { + border-color: var(--divider-color); + border-bottom: none; + margin: 16px 0; + } + ha-expansion-panel { + margin: 16px 0; + } + .row { + margin: 0; + display: flex; + flex-direction: row; + justify-content: space-between; + } + .actions { + margin: 8px 0 0; + display: flex; + flex-wrap: wrap; + justify-content: center; + } + + .actions mwc-button { + margin: 0 4px 4px; + } + a { + color: var(--primary-color); + } + ha-circular-progress { + width: 100%; + justify-content: center; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-update": MoreInfoUpdate; + } +} diff --git a/src/dialogs/more-info/state_more_info_control.ts b/src/dialogs/more-info/state_more_info_control.ts index 02abe5e731..3df5e85b4b 100644 --- a/src/dialogs/more-info/state_more_info_control.ts +++ b/src/dialogs/more-info/state_more_info_control.ts @@ -25,6 +25,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = { script: () => import("./controls/more-info-script"), sun: () => import("./controls/more-info-sun"), timer: () => import("./controls/more-info-timer"), + update: () => import("./controls/more-info-update"), vacuum: () => import("./controls/more-info-vacuum"), water_heater: () => import("./controls/more-info-water_heater"), weather: () => import("./controls/more-info-weather"), diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index 1e44c0fafd..5085aba824 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -132,6 +132,12 @@ export class HaTabsSubpageDataTable extends LitElement { */ @property() public tabs!: PageNavigation[]; + /** + * Force hides the filter menu. + * @type {Boolean} + */ + @property({ type: Boolean }) public hideFilterMenu = false; + @query("ha-data-table", true) private _dataTable!: HaDataTable; public clearSelection() { @@ -195,16 +201,24 @@ export class HaTabsSubpageDataTable extends LitElement { .mainPage=${this.mainPage} .supervisor=${this.supervisor} > -
- ${this.narrow - ? html`
- ${this.numHidden || this.activeFilters - ? html`${this.numHidden || "!"}` - : ""} - -
` - : ""} -
+ ${!this.hideFilterMenu + ? html` +
+ ${this.narrow + ? html` +
+ ${this.numHidden || this.activeFilters + ? html`${this.numHidden || "!"}` + : ""} + +
+ ` + : ""} +
+ ` + : ""} ${this.narrow ? html`
diff --git a/src/onboarding/onboarding-integrations.ts b/src/onboarding/onboarding-integrations.ts index 6ac3504e5e..9fc15b0c07 100644 --- a/src/onboarding/onboarding-integrations.ts +++ b/src/onboarding/onboarding-integrations.ts @@ -169,8 +169,8 @@ class OnboardingIntegrations extends LitElement { } private async _loadConfigEntries() { - const entries = await getConfigEntries(this.hass!); - // We filter out the config entry for the local weather and rpi_power. + const entries = await getConfigEntries(this.hass!, { type: "integration" }); + // We filter out the config entries that are automatically created during onboarding. // It is one that we create automatically and it will confuse the user // if it starts showing up during onboarding. this._entries = entries.filter( diff --git a/src/panels/config/automation/blueprint-automation-editor.ts b/src/panels/config/automation/blueprint-automation-editor.ts index 907ee3bb03..37b08264de 100644 --- a/src/panels/config/automation/blueprint-automation-editor.ts +++ b/src/panels/config/automation/blueprint-automation-editor.ts @@ -326,12 +326,9 @@ export class HaBlueprintAutomationEditor extends LitElement { } ha-settings-row { --paper-time-input-justify-content: flex-end; + --settings-row-content-width: 100%; border-top: 1px solid var(--divider-color); } - :host(:not([narrow])) ha-settings-row ha-textfield, - :host(:not([narrow])) ha-settings-row ha-selector { - width: 60%; - } `, ]; } diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index 4bce08a441..8585e232e9 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -7,12 +7,18 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { handleStructError } from "../../../../common/structs/handle-errors"; import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; +import "../../../../components/buttons/ha-progress-button"; +import type { HaProgressButton } from "../../../../components/buttons/ha-progress-button"; import "../../../../components/ha-icon-button"; -import { Condition } from "../../../../data/automation"; -import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { Condition, testCondition } from "../../../../data/automation"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; import "./ha-automation-condition-editor"; +import { validateConfig } from "../../../../data/config"; export interface ConditionElement extends LitElement { condition: Condition; @@ -61,6 +67,11 @@ export default class HaAutomationConditionRow extends LitElement {
+ + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.test" + )} + Object.values(states).filter( - (entity) => computeStateDomain(entity) === "automation" + (entity) => + computeStateDomain(entity) === "automation" && + !entity.attributes.restored ) as AutomationEntity[] ); @@ -87,7 +89,7 @@ class HaConfigAutomation extends HassRouterPage { (!changedProps || changedProps.has("route")) && this._currentPage !== "dashboard" ) { - const automationId = this.routeTail.path.substr(1); + const automationId = decodeURIComponent(this.routeTail.path.substr(1)); pageEl.automationId = automationId === "new" ? null : automationId; } } diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index 7844e7ba09..3060139753 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -16,12 +16,16 @@ import "../../../../components/ha-alert"; import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; import "../../../../components/ha-icon-button"; +import "../../../../components/ha-yaml-editor"; import "../../../../components/ha-select"; import type { HaSelect } from "../../../../components/ha-select"; import "../../../../components/ha-textfield"; import { subscribeTrigger, Trigger } from "../../../../data/automation"; import { validateConfig } from "../../../../data/config"; -import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import "./types/ha-automation-trigger-device"; @@ -94,7 +98,7 @@ export default class HaAutomationTriggerRow extends LitElement { @state() private _requestShowId = false; - @state() private _triggered = false; + @state() private _triggered?: Record; @state() private _triggerColor = false; @@ -231,9 +235,10 @@ export default class HaAutomationTriggerRow extends LitElement {
${this.hass.localize( "ui.panel.config.automation.editor.triggers.triggered" @@ -288,7 +293,7 @@ export default class HaAutomationTriggerRow extends LitElement { } const validateResult = await validateConfig(this.hass, { - trigger: this.trigger, + trigger, }); // Don't do anything if trigger not valid or if trigger changed. @@ -298,16 +303,16 @@ export default class HaAutomationTriggerRow extends LitElement { const triggerUnsub = subscribeTrigger( this.hass, - () => { + (result) => { if (untriggerTimeout !== undefined) { clearTimeout(untriggerTimeout); this._triggerColor = !this._triggerColor; } else { this._triggerColor = false; } - this._triggered = true; + this._triggered = result; untriggerTimeout = window.setTimeout(() => { - this._triggered = false; + this._triggered = undefined; untriggerTimeout = undefined; }, showTriggeredTime); }, @@ -416,6 +421,18 @@ export default class HaAutomationTriggerRow extends LitElement { this._yamlMode = !this._yamlMode; } + private _showTriggeredInfo() { + showAlertDialog(this, { + text: html` + + `, + }); + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -426,12 +443,12 @@ export default class HaAutomationTriggerRow extends LitElement { --mdc-theme-text-primary-on-background: var(--primary-text-color); } .triggered { + cursor: pointer; position: absolute; top: 0px; right: 0px; left: 0px; text-transform: uppercase; - pointer-events: none; font-weight: bold; font-size: 14px; background-color: var(--primary-color); @@ -446,6 +463,9 @@ export default class HaAutomationTriggerRow extends LitElement { .triggered.active { max-height: 100px; } + .triggered:hover { + opacity: 0.8; + } .triggered.accent { background-color: var(--accent-color); color: var(--text-accent-color, var(--text-primary-color)); diff --git a/src/panels/config/backup/ha-config-backup.ts b/src/panels/config/backup/ha-config-backup.ts new file mode 100644 index 0000000000..514f9f23e3 --- /dev/null +++ b/src/panels/config/backup/ha-config-backup.ts @@ -0,0 +1,224 @@ +import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js"; +import "@polymer/paper-tooltip/paper-tooltip"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoize from "memoize-one"; +import { relativeTime } from "../../../common/datetime/relative_time"; +import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; +import "../../../components/ha-circular-progress"; +import "../../../components/ha-fab"; +import "../../../components/ha-icon"; +import "../../../components/ha-icon-overflow-menu"; +import "../../../components/ha-svg-icon"; +import { getSignedPath } from "../../../data/auth"; +import { + BackupContent, + BackupData, + fetchBackupInfo, + generateBackup, + getBackupDownloadUrl, + removeBackup, +} from "../../../data/backup"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import "../../../layouts/hass-loading-screen"; +import "../../../layouts/hass-tabs-subpage-data-table"; +import { HomeAssistant, Route } from "../../../types"; +import { fileDownload } from "../../../util/file_download"; +import { configSections } from "../ha-panel-config"; + +@customElement("ha-config-backup") +class HaConfigBackup extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public isWide!: boolean; + + @property({ type: Boolean }) public narrow!: boolean; + + @property({ attribute: false }) public route!: Route; + + @state() private _backupData?: BackupData; + + private _columns = memoize( + (narrow, _language): DataTableColumnContainer => ({ + name: { + title: this.hass.localize("ui.panel.config.backup.name"), + sortable: true, + filterable: true, + grows: true, + template: (entry: string, backup: BackupContent) => + html`${entry} +
${backup.path}
`, + }, + size: { + title: this.hass.localize("ui.panel.config.backup.size"), + width: "15%", + hidden: narrow, + filterable: true, + sortable: true, + template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB", + }, + date: { + title: this.hass.localize("ui.panel.config.backup.created"), + width: "15%", + direction: "desc", + hidden: narrow, + filterable: true, + sortable: true, + template: (entry: string) => + relativeTime(new Date(entry), this.hass.locale), + }, + + actions: { + title: "", + width: "15%", + template: (_: string, backup: BackupContent) => + html` this._downloadBackup(backup), + }, + // Delete button + { + path: mdiDelete, + label: this.hass.localize( + "ui.panel.config.backup.remove_backup" + ), + action: () => this._removeBackup(backup), + }, + ]} + style="color: var(--secondary-text-color)" + > + `, + }, + }) + ); + + private _getItems = memoize((backupItems: BackupContent[]) => + backupItems.map((backup) => ({ + name: backup.name, + slug: backup.slug, + date: backup.date, + size: backup.size, + path: backup.path, + })) + ); + + protected render(): TemplateResult { + if (!this.hass || this._backupData === undefined) { + return html``; + } + + return html` + + + ${this._backupData.backing_up + ? html`` + : html``} + + + `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this._getBackups(); + } + + private async _getBackups(): Promise { + this._backupData = await fetchBackupInfo(this.hass); + } + + private async _downloadBackup(backup: BackupContent): Promise { + const signedUrl = await getSignedPath( + this.hass, + getBackupDownloadUrl(backup.slug) + ); + fileDownload(signedUrl.path); + } + + private async _generateBackup(): Promise { + const confirm = await showConfirmationDialog(this, { + title: this.hass.localize("ui.panel.config.backup.create.title"), + text: this.hass.localize("ui.panel.config.backup.create.description"), + confirmText: this.hass.localize("ui.panel.config.backup.create.confirm"), + }); + if (!confirm) { + return; + } + + generateBackup(this.hass) + .then(() => this._getBackups()) + .catch((err) => showAlertDialog(this, { text: (err as Error).message })); + + await this._getBackups(); + } + + private async _removeBackup(backup: BackupContent): Promise { + const confirm = await showConfirmationDialog(this, { + title: this.hass.localize("ui.panel.config.backup.remove.title"), + text: this.hass.localize("ui.panel.config.backup.remove.description", { + name: backup.name, + }), + confirmText: this.hass.localize("ui.panel.config.backup.remove.confirm"), + }); + if (!confirm) { + return; + } + + await removeBackup(this.hass, backup.slug); + await this._getBackups(); + } + + static get styles(): CSSResultGroup { + return [ + css` + ha-fab[disabled] { + --mdc-theme-secondary: var(--disabled-text-color) !important; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-backup": HaConfigBackup; + } +} diff --git a/src/panels/config/core/ha-config-url-form.ts b/src/panels/config/core/ha-config-url-form.ts index df25744fea..e74546ce7b 100644 --- a/src/panels/config/core/ha-config-url-form.ts +++ b/src/panels/config/core/ha-config-url-form.ts @@ -1,12 +1,25 @@ import "@material/mwc-button/mwc-button"; -import "@polymer/paper-input/paper-input"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, property, state } from "lit/decorators"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../components/ha-card"; +import "../../../components/ha-switch"; +import "../../../components/ha-alert"; +import "../../../components/ha-formfield"; +import "../../../components/ha-textfield"; +import type { HaTextField } from "../../../components/ha-textfield"; +import { CloudStatus, fetchCloudStatus } from "../../../data/cloud"; import { saveCoreConfig } from "../../../data/core"; import type { PolymerChangedEvent } from "../../../polymer-types"; import type { HomeAssistant } from "../../../types"; +import { isIPAddress } from "../../../common/string/is_ip_address"; @customElement("ha-config-url-form") class ConfigUrlForm extends LitElement { @@ -20,18 +33,48 @@ class ConfigUrlForm extends LitElement { @state() private _internal_url?: string; + @state() private _cloudStatus?: CloudStatus | null; + + @state() private _showCustomExternalUrl = false; + + @state() private _showCustomInternalUrl = false; + protected render(): TemplateResult { const canEdit = ["storage", "default"].includes( this.hass.config.config_source ); const disabled = this._working || !canEdit; - if (!this.hass.userData?.showAdvanced) { + if (!this.hass.userData?.showAdvanced || this._cloudStatus === undefined) { return html``; } + const internalUrl = this._internalUrlValue; + const externalUrl = this._externalUrlValue; + let hasCloud: boolean; + let remoteEnabled: boolean; + let httpUseHttps: boolean; + + if (this._cloudStatus === null) { + hasCloud = false; + remoteEnabled = false; + httpUseHttps = false; + } else { + httpUseHttps = this._cloudStatus.http_use_ssl; + + if (this._cloudStatus.logged_in) { + hasCloud = true; + remoteEnabled = + this._cloudStatus.active_subscription && + this._cloudStatus.prefs.remote_enabled; + } else { + hasCloud = false; + remoteEnabled = false; + } + } + return html` - +
${!canEdit ? html` @@ -43,46 +86,147 @@ class ConfigUrlForm extends LitElement { ` : ""} ${this._error ? html`
${this._error}
` : ""} -
-
- ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.external_url" - )} -
- - +
+ ${this.hass.localize("ui.panel.config.url.description")}
+ ${hasCloud + ? html` +
+
+ ${this.hass.localize( + "ui.panel.config.url.external_url_label" + )} +
+ + + +
+ ` + : ""} + ${!this._showCustomExternalUrl + ? "" + : html` +
+
+ ${hasCloud + ? "" + : this.hass.localize( + "ui.panel.config.url.external_url_label" + )} +
+ + +
+ `} + ${hasCloud || !isComponentLoaded(this.hass, "cloud") + ? "" + : html` + + `} + ${!this._showCustomExternalUrl && hasCloud + ? html` + ${remoteEnabled + ? "" + : html` + + ${this.hass.localize( + "ui.panel.config.url.ha_cloud_remote_not_enabled" + )} + + + `} + ` + : ""} +
- ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.internal_url" - )} + ${this.hass.localize("ui.panel.config.url.internal_url_label")}
- - + +
+ + ${!this._showCustomInternalUrl + ? "" + : html` +
+
+ + +
+ `} + ${ + // If the user has configured a cert, show an error if + httpUseHttps && // there is no internal url configured + (!internalUrl || + // the internal url does not start with https + !internalUrl.startsWith("https://") || + // the internal url points at an IP address + isIPAddress(new URL(internalUrl).hostname)) + ? html` + + ${this.hass.localize( + "ui.panel.config.url.internal_url_https_error_description" + )} + + ` + : "" + }
@@ -95,6 +239,24 @@ class ConfigUrlForm extends LitElement { `; } + protected override firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + + this._showCustomInternalUrl = this._internalUrlValue !== null; + + if (isComponentLoaded(this.hass, "cloud")) { + fetchCloudStatus(this.hass).then((cloudStatus) => { + if (cloudStatus.logged_in) { + this._cloudStatus = cloudStatus; + this._showCustomExternalUrl = this._externalUrlValue !== null; + } + }); + } else { + this._cloudStatus = null; + this._showCustomExternalUrl = true; + } + } + private get _internalUrlValue() { return this._internal_url !== undefined ? this._internal_url @@ -107,9 +269,17 @@ class ConfigUrlForm extends LitElement { : this.hass.config.external_url; } + private _toggleCloud(ev) { + this._showCustomExternalUrl = !ev.currentTarget.checked; + } + + private _toggleInternalAutomatic(ev) { + this._showCustomInternalUrl = !ev.currentTarget.checked; + } + private _handleChange(ev: PolymerChangedEvent) { - const target = ev.currentTarget as PaperInputElement; - this[`_${target.name}`] = target.value; + const target = ev.currentTarget as HaTextField; + this[`_${target.name}`] = target.value || null; } private async _save() { @@ -117,8 +287,12 @@ class ConfigUrlForm extends LitElement { this._error = undefined; try { await saveCoreConfig(this.hass, { - external_url: this._external_url || null, - internal_url: this._internal_url || null, + external_url: this._showCustomExternalUrl + ? this._external_url || null + : null, + internal_url: this._showCustomInternalUrl + ? this._internal_url || null + : null, }); } catch (err: any) { this._error = err.message || err; @@ -129,11 +303,15 @@ class ConfigUrlForm extends LitElement { static get styles(): CSSResultGroup { return css` + .description { + margin-bottom: 1em; + } .row { display: flex; flex-direction: row; margin: 0 -8px; align-items: center; + padding: 8px 0; } .secondary { @@ -154,6 +332,10 @@ class ConfigUrlForm extends LitElement { .card-actions { text-align: right; } + + a { + color: var(--primary-color); + } `; } } diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 731a5c7079..3f0e3e4849 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -1,3 +1,5 @@ +import type { ActionDetail } from "@material/mwc-list"; +import "@material/mwc-list/mwc-list-item"; import { mdiCloudLock, mdiDotsVertical, @@ -5,10 +7,9 @@ import { mdiMagnify, mdiNewBox, } from "@mdi/js"; -import "@material/mwc-list/mwc-list-item"; -import type { ActionDetail } from "@material/mwc-list"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; +import type { HassEntities } from "home-assistant-js-websocket"; import { css, CSSResultGroup, @@ -18,30 +19,29 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import "../../../components/ha-card"; -import "../../../components/ha-icon-next"; -import "../../../components/ha-icon-button"; -import "../../../components/ha-menu-button"; +import { computeStateDomain } from "../../../common/entity/compute_state_domain"; +import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import "../../../components/ha-button-menu"; +import "../../../components/ha-card"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-icon-next"; +import "../../../components/ha-menu-button"; import "../../../components/ha-svg-icon"; import { CloudStatus } from "../../../data/cloud"; -import { - refreshSupervisorAvailableUpdates, - SupervisorAvailableUpdates, -} from "../../../data/supervisor/root"; +import { updateCanInstall, UpdateEntity } from "../../../data/update"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar"; import "../../../layouts/ha-app-layout"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; +import { showToast } from "../../../util/toast"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import "./ha-config-navigation"; import "./ha-config-updates"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import { showToast } from "../../../util/toast"; -import { documentationUrl } from "../../../util/documentation-url"; const randomTip = (hass: HomeAssistant) => { const weighted: string[] = []; @@ -113,9 +113,6 @@ class HaConfigDashboard extends LitElement { @property() public cloudStatus?: CloudStatus; - // null means not available - @property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null; - @property() public showAdvanced!: boolean; @state() private _tip?: string; @@ -123,6 +120,9 @@ class HaConfigDashboard extends LitElement { private _notifyUpdates = false; protected render(): TemplateResult { + const canInstallUpdates = this._filterUpdateEntitiesWithInstall( + this.hass.states + ); return html` @@ -160,50 +160,47 @@ class HaConfigDashboard extends LitElement { .isWide=${this.isWide} full-width > - ${this.supervisorUpdates === undefined - ? // Hide everything until updates loaded - html`` - : html`${this.supervisorUpdates?.length - ? html` - - ` - : ""} - - ${this.narrow && this.supervisorUpdates?.length - ? html`
- ${this.hass.localize("panel.config")} -
` - : ""} - ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") - ? html` - - ` - : ""} + ${canInstallUpdates.length + ? html` + + ` + : ""} + + ${this.narrow && canInstallUpdates.length + ? html`
+ ${this.hass.localize("panel.config")} +
` + : ""} + ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") + ? html` -
`} + ` + : ""} + +
Tip! @@ -221,11 +218,11 @@ class HaConfigDashboard extends LitElement { this._tip = randomTip(this.hass); } - if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) { + if (!changedProps.has("hass") || !this._notifyUpdates) { return; } this._notifyUpdates = false; - if (this.supervisorUpdates?.length) { + if (this._filterUpdateEntitiesWithInstall(this.hass.states).length) { showToast(this, { message: this.hass.localize( "ui.panel.config.updates.updates_refreshed" @@ -238,6 +235,44 @@ class HaConfigDashboard extends LitElement { } } + private _filterUpdateEntities = memoizeOne((entities: HassEntities) => + ( + Object.values(entities).filter( + (entity) => computeStateDomain(entity) === "update" + ) as UpdateEntity[] + ).sort((a, b) => { + if (a.attributes.title === "Home Assistant Core") { + return -3; + } + if (b.attributes.title === "Home Assistant Core") { + return 3; + } + if (a.attributes.title === "Home Assistant Operating System") { + return -2; + } + if (b.attributes.title === "Home Assistant Operating System") { + return 2; + } + if (a.attributes.title === "Home Assistant Supervisor") { + return -1; + } + if (b.attributes.title === "Home Assistant Supervisor") { + return 1; + } + return caseInsensitiveStringCompare( + a.attributes.title || a.attributes.friendly_name || "", + b.attributes.title || b.attributes.friendly_name || "" + ); + }) + ); + + private _filterUpdateEntitiesWithInstall = memoizeOne( + (entities: HassEntities) => + this._filterUpdateEntities(entities).filter((entity) => + updateCanInstall(entity) + ) + ); + private _showQuickBar(): void { showQuickBar(this, { commandMode: true, @@ -246,20 +281,24 @@ class HaConfigDashboard extends LitElement { } private async _handleMenuAction(ev: CustomEvent) { + const _entities = this._filterUpdateEntities(this.hass.states).map( + (entity) => entity.entity_id + ); switch (ev.detail.index) { case 0: - if (isComponentLoaded(this.hass, "hassio")) { + if (_entities.length) { this._notifyUpdates = true; - await refreshSupervisorAvailableUpdates(this.hass); - fireEvent(this, "ha-refresh-supervisor"); + await this.hass.callService("homeassistant", "update_entity", { + entity_id: _entities, + }); return; } showAlertDialog(this, { title: this.hass.localize( - "ui.panel.config.updates.check_unavailable.title" + "ui.panel.config.updates.no_update_entities.title" ), text: this.hass.localize( - "ui.panel.config.updates.check_unavailable.description" + "ui.panel.config.updates.no_update_entities.description" ), warning: true, }); diff --git a/src/panels/config/dashboard/ha-config-updates.ts b/src/panels/config/dashboard/ha-config-updates.ts index 8c11e4325e..cae7397e58 100644 --- a/src/panels/config/dashboard/ha-config-updates.ts +++ b/src/panels/config/dashboard/ha-config-updates.ts @@ -1,21 +1,14 @@ import "@material/mwc-button/mwc-button"; -import { mdiPackageVariant } from "@mdi/js"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/entity/state-badge"; import "../../../components/ha-alert"; -import "../../../components/ha-logo-svg"; -import "../../../components/ha-svg-icon"; -import { SupervisorAvailableUpdates } from "../../../data/supervisor/root"; -import { HomeAssistant } from "../../../types"; import "../../../components/ha-icon-next"; - -export const SUPERVISOR_UPDATE_NAMES = { - core: "Home Assistant Core", - os: "Home Assistant Operating System", - supervisor: "Home Assistant Supervisor", -}; +import type { UpdateEntity } from "../../../data/update"; +import { HomeAssistant } from "../../../types"; @customElement("ha-config-updates") class HaConfigUpdates extends LitElement { @@ -24,62 +17,60 @@ class HaConfigUpdates extends LitElement { @property({ type: Boolean }) public narrow!: boolean; @property({ attribute: false }) - public supervisorUpdates?: SupervisorAvailableUpdates[] | null; + public updateEntities?: UpdateEntity[]; @state() private _showAll = false; protected render(): TemplateResult { - if (!this.supervisorUpdates?.length) { + if (!this.updateEntities?.length) { return html``; } const updates = - this._showAll || this.supervisorUpdates.length <= 3 - ? this.supervisorUpdates - : this.supervisorUpdates.slice(0, 2); + this._showAll || this.updateEntities.length <= 3 + ? this.updateEntities + : this.updateEntities.slice(0, 2); return html`
${this.hass.localize("ui.panel.config.updates.title", { - count: this.supervisorUpdates.length, + count: this.updateEntities.length, })}
${updates.map( - (update) => html` - - - - ${update.update_type === "addon" - ? update.icon - ? html`` - : html`` - : html``} - - - ${update.update_type === "addon" - ? update.name - : SUPERVISOR_UPDATE_NAMES[update.update_type!]} -
- ${this.hass.localize( - "ui.panel.config.updates.version_available", - { - version_available: update.version_latest, - } - )} -
-
- ${!this.narrow ? html`` : ""} -
-
+ (entity) => html` + + + + + + ${entity.attributes.title || entity.attributes.friendly_name} +
+ ${this.hass.localize( + "ui.panel.config.updates.version_available", + { + version_available: entity.attributes.latest_version, + } + )} +
+
+ ${!this.narrow ? html`` : ""} +
` )} - ${!this._showAll && this.supervisorUpdates.length >= 4 + ${!this._showAll && this.updateEntities.length >= 4 ? html` ` @@ -87,6 +78,12 @@ class HaConfigUpdates extends LitElement { `; } + private _openMoreInfo(ev: MouseEvent): void { + fireEvent(this, "hass-more-info", { + entityId: (ev.currentTarget as any).entity_id, + }); + } + private _showAllClicked() { this._showAll = true; } @@ -99,25 +96,11 @@ class HaConfigUpdates extends LitElement { padding: 16px; padding-bottom: 0; } - a { - text-decoration: none; - color: var(--primary-text-color); - } .icon { display: inline-flex; height: 100%; align-items: center; } - img, - ha-svg-icon, - ha-logo-svg { - --mdc-icon-size: 32px; - max-height: 32px; - width: 32px; - } - ha-logo-svg { - color: var(--secondary-text-color); - } ha-icon-next { color: var(--secondary-text-color); height: 24px; @@ -139,6 +122,9 @@ class HaConfigUpdates extends LitElement { outline: none; text-decoration: underline; } + paper-icon-item { + cursor: pointer; + } `, ]; } 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 ebfbd159dd..19e60da9ee 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 @@ -40,7 +40,7 @@ export class HaDeviceEntitiesCard extends LitElement { @property() public entities!: EntityRegistryStateEntry[]; - @property() public showDisabled = false; + @property() public showHidden = false; @state() private _extDisabledEntityEntries?: Record< string, @@ -60,77 +60,77 @@ export class HaDeviceEntitiesCard extends LitElement { } protected render(): TemplateResult { - const disabledEntities: EntityRegistryStateEntry[] = []; + if (!this.entities.length) { + return html` + +
+ ${this.hass.localize("ui.panel.config.devices.entities.none")} +
+
+ `; + } + + const shownEntities: EntityRegistryStateEntry[] = []; + const hiddenEntities: EntityRegistryStateEntry[] = []; this._entityRows = []; + + this.entities.forEach((entry) => { + if (entry.disabled_by || entry.hidden_by) { + if (this._extDisabledEntityEntries) { + hiddenEntities.push( + this._extDisabledEntityEntries[entry.entity_id] || entry + ); + } else { + hiddenEntities.push(entry); + } + } else { + shownEntities.push(entry); + } + }); + return html` - ${this.entities.length - ? html` -
- ${this.entities.map((entry: EntityRegistryStateEntry) => { - if (entry.disabled_by) { - if (this._extDisabledEntityEntries) { - disabledEntities.push( - this._extDisabledEntityEntries[entry.entity_id] || entry - ); - } else { - disabledEntities.push(entry); - } - return ""; - } - return this.hass.states[entry.entity_id] - ? this._renderEntity(entry) - : this._renderEntry(entry); - })} -
- ${disabledEntities.length - ? !this.showDisabled - ? html` - - ` - : html` - ${disabledEntities.map((entry) => - this._renderEntry(entry) - )} - - ` - : ""} -
- +
+ ${shownEntities.map((entry) => + this.hass.states[entry.entity_id] + ? this._renderEntity(entry) + : this._renderEntry(entry) + )} +
+ ${hiddenEntities.length + ? !this.showHidden + ? html` +
- ` - : html` -
- ${this.hass.localize("ui.panel.config.devices.entities.none")} -
- `} + + ` + : html` + ${hiddenEntities.map((entry) => this._renderEntry(entry))} + + ` + : ""} +
+ + ${this.hass.localize( + "ui.panel.config.devices.entities.add_entities_lovelace" + )} + +
`; } - private _toggleShowDisabled() { - this.showDisabled = !this.showDisabled; - if (!this.showDisabled || this._extDisabledEntityEntries !== undefined) { + private _toggleShowHidden() { + this.showHidden = !this.showHidden; + if (!this.showHidden || this._extDisabledEntityEntries !== undefined) { return; } this._extDisabledEntityEntries = {}; diff --git a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts deleted file mode 100644 index 8b9f4309a9..0000000000 --- a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts +++ /dev/null @@ -1,84 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property } from "lit/decorators"; -import { navigate } from "../../../../../../common/navigate"; -import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; -import { - getIdentifiersFromDevice, - OZWNodeIdentifiers, -} from "../../../../../../data/ozw"; -import { haStyle } from "../../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../../types"; -import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node"; - -@customElement("ha-device-actions-ozw") -export class HaDeviceActionsOzw extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public device!: DeviceRegistryEntry; - - @property() - private node_id = 0; - - @property() - private ozw_instance = 1; - - protected updated(changedProperties: PropertyValues) { - if (changedProperties.has("device")) { - const identifiers: OZWNodeIdentifiers | undefined = - getIdentifiersFromDevice(this.device); - if (!identifiers) { - return; - } - this.ozw_instance = identifiers.ozw_instance; - this.node_id = identifiers.node_id; - } - } - - protected render(): TemplateResult { - if (!this.ozw_instance || !this.node_id) { - return html``; - } - return html` - - ${this.hass.localize("ui.panel.config.ozw.node.button")} - - - ${this.hass.localize("ui.panel.config.ozw.refresh_node.button")} - - `; - } - - private async _refreshNodeClicked() { - showOZWRefreshNodeDialog(this, { - node_id: this.node_id, - ozw_instance: this.ozw_instance, - }); - } - - private async _nodeDetailsClicked() { - navigate( - `/config/ozw/network/${this.ozw_instance}/node/${this.node_id}/dashboard` - ); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - :host { - display: flex; - flex-direction: column; - align-items: flex-start; - } - `, - ]; - } -} diff --git a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts deleted file mode 100644 index 4717d235b8..0000000000 --- a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; -import { - fetchOZWNodeStatus, - getIdentifiersFromDevice, - OZWDevice, - OZWNodeIdentifiers, -} from "../../../../../../data/ozw"; -import { haStyle } from "../../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../../types"; - -@customElement("ha-device-info-ozw") -export class HaDeviceInfoOzw extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public device!: DeviceRegistryEntry; - - @property() - private node_id = 0; - - @property() - private ozw_instance = 1; - - @state() private _ozwDevice?: OZWDevice; - - protected updated(changedProperties: PropertyValues) { - if (changedProperties.has("device")) { - const identifiers: OZWNodeIdentifiers | undefined = - getIdentifiersFromDevice(this.device); - if (!identifiers) { - return; - } - this.ozw_instance = identifiers.ozw_instance; - this.node_id = identifiers.node_id; - - this._fetchNodeDetails(); - } - } - - protected async _fetchNodeDetails() { - this._ozwDevice = await fetchOZWNodeStatus( - this.hass, - this.ozw_instance, - this.node_id - ); - } - - protected render(): TemplateResult { - if (!this._ozwDevice) { - return html``; - } - return html` -

- ${this.hass.localize("ui.panel.config.ozw.device_info.zwave_info")} -

-
- ${this.hass.localize("ui.panel.config.ozw.common.node_id")}: - ${this._ozwDevice.node_id} -
-
- ${this.hass.localize("ui.panel.config.ozw.device_info.stage")}: - ${this._ozwDevice.node_query_stage} -
-
- ${this.hass.localize("ui.panel.config.ozw.common.ozw_instance")}: - ${this._ozwDevice.ozw_instance} -
-
- ${this.hass.localize("ui.panel.config.ozw.device_info.node_failed")}: - ${this._ozwDevice.is_failed - ? this.hass.localize("ui.common.yes") - : this.hass.localize("ui.common.no")} -
- `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - h4 { - margin-bottom: 4px; - } - div { - word-break: break-all; - margin-top: 2px; - } - `, - ]; - } -} diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts index 32a732662f..78d56a0b1b 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts @@ -27,7 +27,7 @@ import { HomeAssistant } from "../../../../../../types"; export class HaDeviceInfoZWaveJS extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public device!: DeviceRegistryEntry; + @property({ attribute: false }) public device!: DeviceRegistryEntry; @state() private _entryId?: string; @@ -58,12 +58,11 @@ export class HaDeviceInfoZWaveJS extends LitElement { return; } - const configEntries = await getConfigEntries(this.hass); + const configEntries = await getConfigEntries(this.hass, { + domain: "zwave_js", + }); let zwaveJsConfEntries = 0; for (const entry of configEntries) { - if (entry.domain !== "zwave_js") { - continue; - } if (zwaveJsConfEntries) { this._multipleConfigEntries = true; } @@ -173,3 +172,9 @@ export class HaDeviceInfoZWaveJS extends LitElement { ]; } } + +declare global { + interface HTMLElementTagNameMap { + "ha-device-info-zwave_js": HaDeviceInfoZWaveJS; + } +} diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 5572834106..b1b8064359 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -33,6 +33,7 @@ import { fetchDiagnosticHandler, getDeviceDiagnosticsDownloadUrl, getConfigEntryDiagnosticsDownloadUrl, + DiagnosticInfo, } from "../../../data/diagnostics"; import { EntityRegistryEntry, @@ -219,22 +220,32 @@ export class HaConfigDevicePage extends LitElement { } let links = await Promise.all( - this._integrations(device, this.entries).map(async (entry) => { - if (entry.state !== "loaded") { - return false; - } - const info = await fetchDiagnosticHandler(this.hass, entry.domain); + this._integrations(device, this.entries).map( + async (entry): Promise => { + if (entry.state !== "loaded") { + return false; + } + let info: DiagnosticInfo; + try { + info = await fetchDiagnosticHandler(this.hass, entry.domain); + } catch (err: any) { + if (err.code === "not_found") { + return false; + } + throw err; + } - if (!info.handlers.device && !info.handlers.config_entry) { - return false; + if (!info.handlers.device && !info.handlers.config_entry) { + return false; + } + return { + link: info.handlers.device + ? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId) + : getConfigEntryDiagnosticsDownloadUrl(entry.entry_id), + domain: entry.domain, + }; } - return { - link: info.handlers.device - ? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId) - : getConfigEntryDiagnosticsDownloadUrl(entry.entry_id), - domain: entry.domain, - }; - }) + ) ); links = links.filter(Boolean); @@ -557,7 +568,7 @@ export class HaConfigDevicePage extends LitElement { )} .deviceName=${deviceName} .entities=${entitiesByCategory[category]} - .showDisabled=${device.disabled_by !== null} + .showHidden=${device.disabled_by !== null} > ` @@ -902,22 +913,6 @@ export class HaConfigDevicePage extends LitElement { > `); } - if (domains.includes("ozw")) { - import("./device-detail/integration-elements/ozw/ha-device-actions-ozw"); - import("./device-detail/integration-elements/ozw/ha-device-info-ozw"); - deviceInfo.push(html` - - `); - deviceActions.push(html` - - `); - } if (domains.includes("zha")) { import("./device-detail/integration-elements/zha/ha-device-actions-zha"); import("./device-detail/integration-elements/zha/ha-device-info-zha"); diff --git a/src/panels/config/energy/components/ha-energy-gas-settings.ts b/src/panels/config/energy/components/ha-energy-gas-settings.ts index 3f92d1a023..4e65150f2b 100644 --- a/src/panels/config/energy/components/ha-energy-gas-settings.ts +++ b/src/panels/config/energy/components/ha-energy-gas-settings.ts @@ -121,6 +121,7 @@ export class EnergyGasSettings extends LitElement { showEnergySettingsGasDialog(this, { unit: getEnergyGasUnitCategory(this.hass, this.preferences), saveCallback: async (source) => { + delete source.unit_of_measurement; await this._savePreferences({ ...this.preferences, energy_sources: this.preferences.energy_sources.concat(source), diff --git a/src/panels/config/energy/components/ha-energy-grid-settings.ts b/src/panels/config/energy/components/ha-energy-grid-settings.ts index 700329dd1c..46ac49d885 100644 --- a/src/panels/config/energy/components/ha-energy-grid-settings.ts +++ b/src/panels/config/energy/components/ha-energy-grid-settings.ts @@ -54,7 +54,7 @@ export class EnergyGridSettings extends LitElement { @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; - @state() private _configEntries?: ConfigEntry[]; + @state() private _co2ConfigEntry?: ConfigEntry; protected firstUpdated() { this._fetchCO2SignalConfigEntries(); @@ -195,28 +195,28 @@ export class EnergyGridSettings extends LitElement { "ui.panel.config.energy.grid.grid_carbon_footprint" )} - ${this._configEntries?.map( - (entry) => html`
- - ${entry.title} - - - - -
` - )} - ${this._configEntries?.length === 0 - ? html` + ${this._co2ConfigEntry + ? html`
+ + ${this._co2ConfigEntry.title} + + + + +
` + : html`
- ` - : ""} + `}
`; } private async _fetchCO2SignalConfigEntries() { - this._configEntries = (await getConfigEntries(this.hass)).filter( - (entry) => entry.domain === "co2signal" - ); + const entries = await getConfigEntries(this.hass, { domain: "co2signal" }); + this._co2ConfigEntry = entries.length ? entries[0] : undefined; } private _addCO2Sensor() { diff --git a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts index 1f2acaf624..8ec83a64b8 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts @@ -176,9 +176,17 @@ export class DialogEnergySolarSettings private async _fetchSolarForecastConfigEntries() { const domains = this._params!.info.solar_forecast_domains; - this._configEntries = (await getConfigEntries(this.hass)).filter((entry) => - domains.includes(entry.domain) - ); + this._configEntries = + domains.length === 0 + ? [] + : domains.length === 1 + ? await getConfigEntries(this.hass, { + type: "integration", + domain: domains[0], + }) + : (await getConfigEntries(this.hass, { type: "integration" })).filter( + (entry) => domains.includes(entry.domain) + ); } private _handleForecastChanged(ev: CustomEvent) { diff --git a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts index 913bcd8353..210d931a6c 100644 --- a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts +++ b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts @@ -10,50 +10,11 @@ import { customElement, property, state, query } from "lit/decorators"; import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import { - deleteCounter, - fetchCounter, - updateCounter, -} from "../../../../../data/counter"; import { ExtEntityRegistryEntry, removeEntityRegistryEntry, } from "../../../../../data/entity_registry"; -import { - deleteInputBoolean, - fetchInputBoolean, - updateInputBoolean, -} from "../../../../../data/input_boolean"; -import { - deleteInputButton, - fetchInputButton, - updateInputButton, -} from "../../../../../data/input_button"; -import { - deleteInputDateTime, - fetchInputDateTime, - updateInputDateTime, -} from "../../../../../data/input_datetime"; -import { - deleteInputNumber, - fetchInputNumber, - updateInputNumber, -} from "../../../../../data/input_number"; -import { - deleteInputSelect, - fetchInputSelect, - updateInputSelect, -} from "../../../../../data/input_select"; -import { - deleteInputText, - fetchInputText, - updateInputText, -} from "../../../../../data/input_text"; -import { - deleteTimer, - fetchTimer, - updateTimer, -} from "../../../../../data/timer"; +import { HELPERS_CRUD } from "../../../../../data/helpers_crud"; import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; @@ -69,49 +30,6 @@ import "../../../helpers/forms/ha-timer-form"; import "../../entity-registry-basic-editor"; import type { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor"; -const HELPERS = { - input_boolean: { - fetch: fetchInputBoolean, - update: updateInputBoolean, - delete: deleteInputBoolean, - }, - input_button: { - fetch: fetchInputButton, - update: updateInputButton, - delete: deleteInputButton, - }, - input_text: { - fetch: fetchInputText, - update: updateInputText, - delete: deleteInputText, - }, - input_number: { - fetch: fetchInputNumber, - update: updateInputNumber, - delete: deleteInputNumber, - }, - input_datetime: { - fetch: fetchInputDateTime, - update: updateInputDateTime, - delete: deleteInputDateTime, - }, - input_select: { - fetch: fetchInputSelect, - update: updateInputSelect, - delete: deleteInputSelect, - }, - counter: { - fetch: fetchCounter, - update: updateCounter, - delete: deleteCounter, - }, - timer: { - fetch: fetchTimer, - update: updateTimer, - delete: deleteTimer, - }, -}; - @customElement("entity-settings-helper-tab") export class EntityRegistrySettingsHelper extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -198,7 +116,7 @@ export class EntityRegistrySettingsHelper extends LitElement { } private async _getItem() { - const items = await HELPERS[this.entry.platform].fetch(this.hass!); + const items = await HELPERS_CRUD[this.entry.platform].fetch(this.hass!); this._item = items.find((item) => item.id === this.entry.unique_id) || null; } @@ -206,7 +124,7 @@ export class EntityRegistrySettingsHelper extends LitElement { this._submitting = true; try { if (this._componentLoaded && this._item) { - await HELPERS[this.entry.platform].update( + await HELPERS_CRUD[this.entry.platform].update( this.hass!, this._item.id, this._item @@ -236,7 +154,10 @@ export class EntityRegistrySettingsHelper extends LitElement { try { if (this._componentLoaded && this._item) { - await HELPERS[this.entry.platform].delete(this.hass!, this._item.id); + await HELPERS_CRUD[this.entry.platform].delete( + this.hass!, + this._item.id + ); } else { const stateObj = this.hass.states[this.entry.entity_id]; if (!stateObj?.attributes.restored) { diff --git a/src/panels/config/entities/entity-registry-basic-editor.ts b/src/panels/config/entities/entity-registry-basic-editor.ts index 52240794b3..de7d374318 100644 --- a/src/panels/config/entities/entity-registry-basic-editor.ts +++ b/src/panels/config/entities/entity-registry-basic-editor.ts @@ -1,11 +1,13 @@ +import "@material/mwc-formfield/mwc-formfield"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import "../../../components/ha-area-picker"; +import "../../../components/ha-expansion-panel"; +import "../../../components/ha-radio"; import "../../../components/ha-switch"; import "../../../components/ha-textfield"; -import type { HaSwitch } from "../../../components/ha-switch"; import { DeviceRegistryEntry, subscribeDeviceRegistry, @@ -33,6 +35,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { @state() private _disabledBy!: string | null; + @state() private _hiddenBy!: string | null; + private _deviceLookup?: Record; @state() private _device?: DeviceRegistryEntry; @@ -51,6 +55,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { ) { params.disabled_by = this._disabledBy; } + if ( + this.entry.hidden_by !== this._hiddenBy && + (this._hiddenBy === null || this._hiddenBy === "user") + ) { + params.hidden_by = this._hiddenBy; + } try { const result = await updateEntityRegistryEntry( this.hass!, @@ -101,6 +111,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { this._origEntityId = this.entry.entity_id; this._entityId = this.entry.entity_id; this._disabledBy = this.entry.disabled_by; + this._hiddenBy = this.entry.hidden_by; this._areaId = this.entry.area_id; this._device = this.entry.device_id && this._deviceLookup @@ -138,37 +149,104 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { .placeholder=${this._device?.area_id} @value-changed=${this._areaPicked} > -
- - -
-
- ${this.hass.localize( + + +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.entity_status" + )}: +
+
+ ${this._disabledBy && this._disabledBy !== "user" + ? this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_cause", + "cause", + this.hass.localize( + `config_entry.disabled_by.${this._disabledBy}` + ) + ) + : ""} +
+
+ -
- ${this._disabledBy && this._disabledBy !== "user" - ? this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_cause", - "cause", - this.hass.localize( - `config_entry.disabled_by.${this._disabledBy}` - ) - ) - : ""} - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_description" + > + + + ${this.hass.localize( - "ui.dialogs.entity_registry.editor.note" + > + + + + > + +
-
+ + ${this._disabledBy !== null + ? html` +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_description" + )} +
+ ` + : this._hiddenBy !== null + ? html` +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.hidden_description" + )} +
+ ` + : ""} +
`; } @@ -180,8 +258,21 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { this._entityId = ev.target.value; } - private _disabledByChanged(ev: Event): void { - this._disabledBy = (ev.target as HaSwitch).checked ? null : "user"; + private _viewStatusChanged(ev: CustomEvent): void { + switch ((ev.target as any).value) { + case "enabled": + this._disabledBy = null; + this._hiddenBy = null; + break; + case "disabled": + this._disabledBy = "user"; + this._hiddenBy = null; + break; + case "hidden": + this._hiddenBy = "user"; + this._disabledBy = null; + break; + } } static get styles() { @@ -202,6 +293,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { display: block; margin-bottom: 8px; } + ha-expansion-panel { + margin-top: 8px; + } + .label { + margin-top: 16px; + } `; } } diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 72854d8389..48ebb5df49 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -1,4 +1,5 @@ import "@material/mwc-button/mwc-button"; +import "@material/mwc-formfield/mwc-formfield"; import "@material/mwc-list/mwc-list-item"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { @@ -18,10 +19,20 @@ import "../../../components/ha-alert"; import "../../../components/ha-area-picker"; import "../../../components/ha-expansion-panel"; import "../../../components/ha-icon-picker"; +import "../../../components/ha-radio"; import "../../../components/ha-select"; import "../../../components/ha-switch"; -import type { HaSwitch } from "../../../components/ha-switch"; import "../../../components/ha-textfield"; +import { + ConfigEntry, + deleteConfigEntry, + getConfigEntries, +} from "../../../data/config_entries"; +import { + createConfigFlow, + handleConfigFlowStep, +} from "../../../data/config_flow"; +import { DataEntryFlowStepCreateEntry } from "../../../data/data_entry_flow"; import { DeviceRegistryEntry, subscribeDeviceRegistry, @@ -30,9 +41,12 @@ import { import { EntityRegistryEntryUpdateParams, ExtEntityRegistryEntry, + fetchEntityRegistry, removeEntityRegistryEntry, updateEntityRegistryEntry, } from "../../../data/entity_registry"; +import { domainToName } from "../../../data/integration"; +import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; import { showAlertDialog, showConfirmationDialog, @@ -41,12 +55,49 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; +import { showEntityEditorDialog } from "./show-dialog-entity-editor"; const OVERRIDE_DEVICE_CLASSES = { - cover: ["window", "door", "garage", "gate"], - binary_sensor: ["window", "door", "garage_door", "opening"], + cover: [ + [ + "awning", + "blind", + "curtain", + "damper", + "door", + "garage", + "gate", + "shade", + "shutter", + "window", + ], + ], + binary_sensor: [ + ["lock"], // Lock + ["window", "door", "garage_door", "opening"], // Door + ["battery", "battery_charging"], // Battery + ["cold", "gas", "heat"], // Climate + ["running", "motion", "moving", "occupancy", "presence", "vibration"], // Presence + ["power", "plug", "light"], // Power + [ + "smoke", + "safety", + "sound", + "problem", + "tamper", + "carbon_monoxide", + "moisture", + ], // Alarm + ], }; +const OVERRIDE_SENSOR_UNITS = { + temperature: ["°C", "°F", "K"], + pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], +}; + +const SWITCH_AS_DOMAINS = ["light", "lock", "cover", "fan", "siren"]; + @customElement("entity-registry-settings") export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -61,20 +112,30 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @state() private _deviceClass?: string; + @state() private _switchAs = "switch"; + @state() private _areaId?: string | null; @state() private _disabledBy!: string | null; - private _deviceLookup?: Record; + @state() private _hiddenBy!: string | null; @state() private _device?: DeviceRegistryEntry; + @state() private _helperConfigEntry?: ConfigEntry; + + @state() private _unit_of_measurement?: string | null; + @state() private _error?: string; @state() private _submitting?: boolean; private _origEntityId!: string; + private _deviceLookup?: Record; + + private _deviceClassOptions?: string[][]; + public hassSubscribe(): UnsubscribeFunc[] { return [ subscribeDeviceRegistry(this.hass.connection!, (devices) => { @@ -89,22 +150,62 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { ]; } - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (changedProperties.has("entry")) { - this._error = undefined; - this._name = this.entry.name || ""; - this._icon = this.entry.icon || ""; - this._deviceClass = - this.entry.device_class || this.entry.original_device_class; - this._origEntityId = this.entry.entity_id; - this._areaId = this.entry.area_id; - this._entityId = this.entry.entity_id; - this._disabledBy = this.entry.disabled_by; - this._device = - this.entry.device_id && this._deviceLookup - ? this._deviceLookup[this.entry.device_id] - : undefined; + protected firstUpdated(changedProps: PropertyValues): void { + super.firstUpdated(changedProps); + if (this.entry.config_entry_id) { + getConfigEntries(this.hass, { + type: "helper", + domain: this.entry.platform, + }).then((entries) => { + this._helperConfigEntry = entries.find( + (ent) => ent.entry_id === this.entry.config_entry_id + ); + }); + } + } + + protected willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + if (!changedProperties.has("entry")) { + return; + } + + this._error = undefined; + this._name = this.entry.name || ""; + this._icon = this.entry.icon || ""; + this._deviceClass = + this.entry.device_class || this.entry.original_device_class; + this._origEntityId = this.entry.entity_id; + this._areaId = this.entry.area_id; + this._entityId = this.entry.entity_id; + this._disabledBy = this.entry.disabled_by; + this._hiddenBy = this.entry.hidden_by; + this._device = + this.entry.device_id && this._deviceLookup + ? this._deviceLookup[this.entry.device_id] + : undefined; + + const domain = computeDomain(this.entry.entity_id); + + if (domain === "sensor") { + const stateObj: HassEntity | undefined = + this.hass.states[this.entry.entity_id]; + this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement; + } + + const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain]; + + if (!deviceClasses) { + return; + } + + this._deviceClassOptions = [[], []]; + for (const deviceClass of deviceClasses) { + if (deviceClass.includes(this.entry.original_device_class!)) { + this._deviceClassOptions[0] = deviceClass; + } else { + this._deviceClassOptions[1].push(...deviceClass); + } } } @@ -160,24 +261,81 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { : undefined} .disabled=${this._submitting} > - ${OVERRIDE_DEVICE_CLASSES[domain]?.includes(this._deviceClass) || - (domain === "cover" && this.entry.original_device_class === null) + ${this._deviceClassOptions + ? html` + + ${this._deviceClassOptions[0].map( + (deviceClass: string) => html` + + ${this.hass.localize( + `ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}` + )} + + ` + )} +
  • + ${this._deviceClassOptions[1].map( + (deviceClass: string) => html` + + ${this.hass.localize( + `ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}` + )} + + ` + )} +
    + ` + : ""} + ${this._deviceClass && + stateObj.attributes.unit_of_measurement && + OVERRIDE_SENSOR_UNITS[this._deviceClass]?.includes( + stateObj.attributes.unit_of_measurement + ) + ? html` + + ${OVERRIDE_SENSOR_UNITS[this._deviceClass].map( + (unit: string) => html` + ${unit} + ` + )} + + ` + : ""} + ${domain === "switch" ? html` - ${OVERRIDE_DEVICE_CLASSES[domain].map( - (deviceClass: string) => html` - - ${this.hass.localize( - `ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}` - )} + + ${domainToName(this.hass.localize, "switch")} + ${SWITCH_AS_DOMAINS.map( + (as_domain) => html` + + ${domainToName(this.hass.localize, as_domain)} ` )} @@ -200,82 +358,156 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { @value-changed=${this._areaPicked} >` : ""} -
    - - -
    -
    - ${this.hass.localize( + ${this._helperConfigEntry + ? html` +
    + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.configure_state" + )} + +
    + ` + : ""} + + +
    + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.entity_status" + )}: +
    +
    + ${this._disabledBy && + this._disabledBy !== "user" && + this._disabledBy !== "integration" + ? this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_cause", + "cause", + this.hass.localize( + `config_entry.disabled_by.${this._disabledBy}` + ) + ) + : ""} +
    +
    + -
    - ${this._disabledBy && this._disabledBy !== "user" - ? this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_cause", - "cause", - this.hass.localize( - `config_entry.disabled_by.${this._disabledBy}` - ) - ) - : ""} - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.enabled_description" - )} -
    ${this.hass.localize( - "ui.dialogs.entity_registry.editor.note" - )} -
    -
    -
    - - ${this.entry.device_id - ? html` -

    - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.area_note" - )} -

    - ${this._areaId - ? html`${this.hass.localize( - "ui.dialogs.entity_registry.editor.follow_device_area" - )}` - : this._device - ? html`${this.hass.localize( - "ui.dialogs.entity_registry.editor.change_device_area" - )}` - : ""} -
    ` - : ""} + + + + + + + + +
    + + ${this._disabledBy !== null + ? html` +
    + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_description" + )} +
    + ` + : this._hiddenBy !== null + ? html` +
    + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.hidden_description" + )} +
    + ` + : ""} + ${this.entry.device_id + ? html` +
    + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.change_area" + )}: +
    + +
    + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.area_note" + )} + ${this._device + ? html` + + ` + : ""} +
    + ` + : ""} +
    ${this.hass.localize("ui.dialogs.entity_registry.editor.delete")} @@ -309,14 +541,38 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { this._deviceClass = ev.target.value; } + private _unitChanged(ev): void { + this._error = undefined; + this._unit_of_measurement = ev.target.value; + } + + private _switchAsChanged(ev): void { + if (ev.target.value === "") { + return; + } + this._switchAs = ev.target.value; + } + private _areaPicked(ev: CustomEvent) { this._error = undefined; this._areaId = ev.detail.value; } - private _clearArea() { - this._error = undefined; - this._areaId = null; + private _viewStatusChanged(ev: CustomEvent): void { + switch ((ev.target as any).value) { + case "enabled": + this._disabledBy = null; + this._hiddenBy = null; + break; + case "disabled": + this._disabledBy = "user"; + this._hiddenBy = null; + break; + case "hidden": + this._hiddenBy = "user"; + this._disabledBy = null; + break; + } } private _openDeviceSettings() { @@ -330,6 +586,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { private async _updateEntry(): Promise { this._submitting = true; + + const parent = (this.getRootNode() as ShadowRoot).host as HTMLElement; + const params: Partial = { name: this._name.trim() || null, icon: this._icon.trim() || null, @@ -337,12 +596,30 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { device_class: this._deviceClass || null, new_entity_id: this._entityId.trim(), }; + + const stateObj: HassEntity | undefined = + this.hass.states[this.entry.entity_id]; + const domain = computeDomain(this.entry.entity_id); + if ( this.entry.disabled_by !== this._disabledBy && (this._disabledBy === null || this._disabledBy === "user") ) { params.disabled_by = this._disabledBy; } + if ( + this.entry.hidden_by !== this._hiddenBy && + (this._hiddenBy === null || this._hiddenBy === "user") + ) { + params.hidden_by = this._hiddenBy; + } + if ( + domain === "sensor" && + stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement + ) { + params.options_domain = "sensor"; + params.options = { unit_of_measurement: this._unit_of_measurement }; + } try { const result = await updateEntityRegistryEntry( this.hass!, @@ -371,6 +648,46 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { } finally { this._submitting = false; } + + if (this._switchAs !== "switch") { + if ( + !(await showConfirmationDialog(this, { + text: this.hass!.localize( + "ui.dialogs.entity_registry.editor.switch_as_x_confirm", + "domain", + this._switchAs + ), + })) + ) { + return; + } + const configFlow = await createConfigFlow(this.hass, "switch_as_x"); + const result = (await handleConfigFlowStep( + this.hass, + configFlow.flow_id, + { + entity_id: this._entityId.trim(), + target_domain: this._switchAs, + } + )) as DataEntryFlowStepCreateEntry; + if (!result.result?.entry_id) { + return; + } + const unsub = await this.hass.connection.subscribeEvents(() => { + unsub(); + fetchEntityRegistry(this.hass.connection).then((entityRegistry) => { + const entity = entityRegistry.find( + (reg) => reg.config_entry_id === result.result!.entry_id + ); + if (!entity) { + return; + } + showEntityEditorDialog(parent, { + entity_id: entity.entity_id, + }); + }); + }, "entity_registry_updated"); + } } private async _confirmDeleteEntry(): Promise { @@ -387,15 +704,19 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { this._submitting = true; try { - await removeEntityRegistryEntry(this.hass!, this._origEntityId); + if (this._helperConfigEntry) { + await deleteConfigEntry(this.hass, this._helperConfigEntry.entry_id); + } else { + await removeEntityRegistryEntry(this.hass!, this._origEntityId); + } fireEvent(this, "close-dialog"); } finally { this._submitting = false; } } - private _disabledByChanged(ev: Event): void { - this._disabledBy = (ev.target as HaSwitch).checked ? null : "user"; + private async _showOptionsFlow() { + showOptionsFlowDialog(this, this._helperConfigEntry!); } static get styles(): CSSResultGroup { @@ -435,14 +756,25 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { display: block; margin: 8px 0; } + ha-area-picker { + margin: 8px 0; + display: block; + } .row { margin: 8px 0; color: var(--primary-text-color); display: flex; align-items: center; } - p { + .label { + margin-top: 16px; + } + .secondary { margin: 8px 0; + width: 340px; + } + li[divider] { + border-bottom-color: var(--divider-color); } `, ]; diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index f93930d2d3..9cfb164d8f 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -3,6 +3,7 @@ import { mdiAlertCircle, mdiCancel, mdiDelete, + mdiEyeOff, mdiFilterVariant, mdiPencilOff, mdiPlus, @@ -28,9 +29,9 @@ import type { SelectionChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/ha-button-menu"; +import "../../../components/ha-check-list-item"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; -import "../../../components/ha-check-list-item"; import { AreaRegistryEntry, subscribeAreaRegistry, @@ -101,6 +102,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { @state() private _showDisabled = false; + @state() private _showHidden = false; + @state() private _showUnavailable = true; @state() private _showReadOnly = true; @@ -249,7 +252,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { filterable: true, width: "68px", template: (_status, entity: EntityRow) => - entity.unavailable || entity.disabled_by || entity.readonly + entity.unavailable || + entity.disabled_by || + entity.hidden_by || + entity.readonly ? html`
    @@ -280,6 +288,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ? this.hass.localize( "ui.panel.config.entities.picker.status.disabled" ) + : entity.hidden_by + ? this.hass.localize( + "ui.panel.config.entities.picker.status.hidden" + ) : this.hass.localize( "ui.panel.config.entities.picker.status.readonly" )} @@ -301,6 +313,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { showDisabled: boolean, showUnavailable: boolean, showReadOnly: boolean, + showHidden: boolean, entries?: ConfigEntry[] ) => { const result: EntityRow[] = []; @@ -362,6 +375,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ); } + if (!showHidden) { + filteredEntities = filteredEntities.filter( + (entity) => !entity.hidden_by + ); + } + for (const entry of filteredEntities) { const entity = this.hass.states[entry.entity_id]; const unavailable = entity?.state === UNAVAILABLE; @@ -465,6 +484,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { this._showDisabled, this._showUnavailable, this._showReadOnly, + this._showHidden, this._entries ); @@ -487,6 +507,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { .data=${filteredEntities} .activeFilters=${activeFilters} .numHidden=${this._numHiddenEntities} + .hideFilterMenu=${this._selectedEntities.length > 0} .searchLabel=${this.hass.localize( "ui.panel.config.entities.picker.search" )} @@ -506,124 +527,155 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { .hasFab=${includeZHAFab} > ${this._selectedEntities.length - ? html`
    -

    - ${this.hass.localize( - "ui.panel.config.entities.picker.selected", - "number", - this._selectedEntities.length - )} -

    -
    - ${!this.narrow - ? html` - ${this.hass.localize( - "ui.panel.config.entities.picker.enable_selected.button" - )} - ${this.hass.localize( - "ui.panel.config.entities.picker.disable_selected.button" - )} - ${this.hass.localize( - "ui.panel.config.entities.picker.remove_selected.button" - )} - ` - : html` - - - ${this.hass.localize( - "ui.panel.config.entities.picker.enable_selected.button" - )} - - - - ${this.hass.localize( - "ui.panel.config.entities.picker.disable_selected.button" - )} - - - - ${this.hass.localize( - "ui.panel.config.entities.picker.remove_selected.button" - )} - - `} + ? html` +
    +

    + ${this.hass.localize( + "ui.panel.config.entities.picker.selected", + "number", + this._selectedEntities.length + )} +

    +
    + ${!this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.entities.picker.enable_selected.button" + )} + ${this.hass.localize( + "ui.panel.config.entities.picker.disable_selected.button" + )} + ${this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.button" + )} + ${this.hass.localize( + "ui.panel.config.entities.picker.remove_selected.button" + )} + ` + : html` + + + ${this.hass.localize( + "ui.panel.config.entities.picker.enable_selected.button" + )} + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.disable_selected.button" + )} + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.button" + )} + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.remove_selected.button" + )} + + `} +
    -
    ` - : html` - - ${this.narrow && activeFilters?.length - ? html`${this.hass.localize( - "ui.components.data-table.filtering_by" - )} - ${activeFilters.join(", ")} - Clear` - : ""} - - ${this.hass!.localize( - "ui.panel.config.entities.picker.filter.show_disabled" - )} - - - ${this.hass!.localize( - "ui.panel.config.entities.picker.filter.show_unavailable" - )} - - - ${this.hass!.localize( - "ui.panel.config.entities.picker.filter.show_readonly" - )} - - `} + ` + : html` + + + ${this.narrow && activeFilters?.length + ? html`${this.hass.localize( + "ui.components.data-table.filtering_by" + )} + ${activeFilters.join(", ")} + Clear` + : ""} + + ${this.hass!.localize( + "ui.panel.config.entities.picker.filter.show_disabled" + )} + + + ${this.hass!.localize( + "ui.panel.config.entities.picker.filter.show_hidden" + )} + + + ${this.hass!.localize( + "ui.panel.config.entities.picker.filter.show_unavailable" + )} + + + ${this.hass!.localize( + "ui.panel.config.entities.picker.filter.show_readonly" + )} + + + `} ${includeZHAFab ? html` ) { + if (ev.detail.source !== "property") { + return; + } + this._showHidden = ev.detail.selected; + } + private _showRestoredChanged(ev: CustomEvent) { if (ev.detail.source !== "property") { return; @@ -791,6 +851,29 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { }); } + private _hideSelected() { + showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.confirm_title", + "number", + this._selectedEntities.length + ), + text: this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.confirm_text" + ), + confirmText: this.hass.localize("ui.common.hide"), + dismissText: this.hass.localize("ui.common.cancel"), + confirm: () => { + this._selectedEntities.forEach((entity) => + updateEntityRegistryEntry(this.hass, entity, { + hidden_by: "user", + }) + ); + this._clearSelection(); + }, + }); + } + private _removeSelected() { const removeableEntities = this._selectedEntities.filter((entity) => { const stateObj = this.hass.states[entity]; @@ -898,6 +981,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { .header-toolbar .header-btns { margin-right: -12px; } + .header-btns { + display: flex; + } .header-btns > mwc-button, .header-btns > ha-icon-button { margin: 8px; diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index c938ca0975..7483960acd 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -1,5 +1,6 @@ import { mdiAccount, + mdiBackupRestore, mdiBadgeAccountHorizontal, mdiCellphoneCog, mdiCog, @@ -27,10 +28,6 @@ import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { listenMediaQuery } from "../../common/dom/media_query"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; -import { - fetchSupervisorAvailableUpdates, - SupervisorAvailableUpdates, -} from "../../data/supervisor/root"; import "../../layouts/hass-loading-screen"; import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { PageNavigation } from "../../layouts/hass-tabs-subpage"; @@ -67,6 +64,13 @@ export const configSections: { [name: string]: PageNavigation[] } = { iconColor: "#64B5F6", component: "blueprint", }, + { + path: "/config/backup", + translationKey: "backup", + iconPath: mdiBackupRestore, + iconColor: "#4084CD", + component: "backup", + }, { path: "/hassio", translationKey: "supervisor", @@ -109,6 +113,15 @@ export const configSections: { [name: string]: PageNavigation[] } = { core: true, }, ], + backup: [ + { + path: "/config/backup", + translationKey: "ui.panel.config.backup.caption", + iconPath: mdiBackupRestore, + iconColor: "#4084CD", + component: "backup", + }, + ], devices: [ { component: "integrations", @@ -291,6 +304,10 @@ class HaPanelConfig extends HassRouterPage { tag: "ha-config-automation", load: () => import("./automation/ha-config-automation"), }, + backup: { + tag: "ha-config-backup", + load: () => import("./backup/ha-config-backup"), + }, blueprint: { tag: "ha-config-blueprint", load: () => import("./blueprint/ha-config-blueprint"), @@ -376,21 +393,11 @@ class HaPanelConfig extends HassRouterPage { "./integrations/integration-panels/zha/zha-config-dashboard-router" ), }, - zwave: { - tag: "zwave-config-router", - load: () => - import("./integrations/integration-panels/zwave/zwave-config-router"), - }, mqtt: { tag: "mqtt-config-panel", load: () => import("./integrations/integration-panels/mqtt/mqtt-config-panel"), }, - ozw: { - tag: "ozw-config-router", - load: () => - import("./integrations/integration-panels/ozw/ozw-config-router"), - }, zwave_js: { tag: "zwave_js-config-router", load: () => @@ -407,8 +414,6 @@ class HaPanelConfig extends HassRouterPage { @state() private _cloudStatus?: CloudStatus; - @state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null; - private _listeners: Array<() => void> = []; public connectedCallback() { @@ -443,19 +448,7 @@ class HaPanelConfig extends HassRouterPage { } }); } - if (isComponentLoaded(this.hass, "hassio")) { - this._loadSupervisorUpdates(); - this.addEventListener("ha-refresh-supervisor", () => { - this._loadSupervisorUpdates(); - }); - this.addEventListener("connection-status", (ev) => { - if (ev.detail === "connected") { - this._loadSupervisorUpdates(); - } - }); - } else { - this._supervisorUpdates = null; - } + this.addEventListener("ha-refresh-cloud-status", () => this._updateCloudStatus() ); @@ -486,7 +479,6 @@ class HaPanelConfig extends HassRouterPage { isWide, narrow: this.narrow, cloudStatus: this._cloudStatus, - supervisorUpdates: this._supervisorUpdates, }); } else { el.route = this.routeTail; @@ -495,7 +487,6 @@ class HaPanelConfig extends HassRouterPage { el.isWide = isWide; el.narrow = this.narrow; el.cloudStatus = this._cloudStatus; - el.supervisorUpdates = this._supervisorUpdates; } } @@ -513,16 +504,6 @@ class HaPanelConfig extends HassRouterPage { setTimeout(() => this._updateCloudStatus(), 5000); } } - - private async _loadSupervisorUpdates(): Promise { - try { - this._supervisorUpdates = await fetchSupervisorAvailableUpdates( - this.hass - ); - } catch (err) { - this._supervisorUpdates = null; - } - } } declare global { diff --git a/src/panels/config/helpers/const.ts b/src/panels/config/helpers/const.ts index 2e927f66ad..c103332573 100644 --- a/src/panels/config/helpers/const.ts +++ b/src/panels/config/helpers/const.ts @@ -1,11 +1,11 @@ -import { Counter } from "../../../data/counter"; -import { InputBoolean } from "../../../data/input_boolean"; -import { InputButton } from "../../../data/input_button"; -import { InputDateTime } from "../../../data/input_datetime"; -import { InputNumber } from "../../../data/input_number"; -import { InputSelect } from "../../../data/input_select"; -import { InputText } from "../../../data/input_text"; -import { Timer } from "../../../data/timer"; +import type { Counter } from "../../../data/counter"; +import type { InputBoolean } from "../../../data/input_boolean"; +import type { InputButton } from "../../../data/input_button"; +import type { InputDateTime } from "../../../data/input_datetime"; +import type { InputNumber } from "../../../data/input_number"; +import type { InputSelect } from "../../../data/input_select"; +import type { InputText } from "../../../data/input_text"; +import type { Timer } from "../../../data/timer"; export const HELPER_DOMAINS = [ "input_boolean", diff --git a/src/panels/config/helpers/dialog-helper-detail.ts b/src/panels/config/helpers/dialog-helper-detail.ts index bbd0b645e1..a1e303c06b 100644 --- a/src/panels/config/helpers/dialog-helper-detail.ts +++ b/src/panels/config/helpers/dialog-helper-detail.ts @@ -6,8 +6,9 @@ import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; -import { domainIcon } from "../../../common/entity/domain_icon"; import "../../../components/ha-dialog"; +import "../../../components/ha-circular-progress"; +import { getConfigFlowHandlers } from "../../../data/config_flow"; import { createCounter } from "../../../data/counter"; import { createInputBoolean } from "../../../data/input_boolean"; import { createInputButton } from "../../../data/input_button"; @@ -16,6 +17,7 @@ import { createInputNumber } from "../../../data/input_number"; import { createInputSelect } from "../../../data/input_select"; import { createInputText } from "../../../data/input_text"; import { createTimer } from "../../../data/timer"; +import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { Helper } from "./const"; @@ -27,6 +29,9 @@ import "./forms/ha-input_number-form"; import "./forms/ha-input_select-form"; import "./forms/ha-input_text-form"; import "./forms/ha-timer-form"; +import { domainToName } from "../../../data/integration"; +import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail"; +import { brandsUrl } from "../../../util/brands-url"; const HELPERS = { input_boolean: createInputBoolean, @@ -47,7 +52,7 @@ export class DialogHelperDetail extends LitElement { @state() private _opened = false; - @state() private _platform?: string; + @state() private _domain?: string; @state() private _error?: string; @@ -55,102 +60,142 @@ export class DialogHelperDetail extends LitElement { @query(".form") private _form?: HTMLDivElement; - public async showDialog(): Promise { - this._platform = undefined; + @state() private _helperFlows?: string[]; + + private _params?: ShowDialogHelperDetailParams; + + public async showDialog(params: ShowDialogHelperDetailParams): Promise { + this._params = params; + this._domain = params.domain; this._item = undefined; this._opened = true; await this.updateComplete; + Promise.all([ + getConfigFlowHandlers(this.hass, "helper"), + // Ensure the titles are loaded before we render the flows. + this.hass.loadBackendTranslation("title", undefined, true), + ]).then(([flows]) => { + this._helperFlows = flows; + }); } public closeDialog(): void { this._opened = false; this._error = ""; + this._params = undefined; } protected render(): TemplateResult { + let content: TemplateResult; + + if (this._domain) { + content = html` +
    + ${this._error ? html`
    ${this._error}
    ` : ""} + ${dynamicElement(`ha-${this._domain}-form`, { + hass: this.hass, + item: this._item, + new: true, + })} +
    + + ${this.hass!.localize("ui.panel.config.helpers.dialog.create")} + + + ${this.hass!.localize("ui.common.back")} + + `; + } else if (this._helperFlows === undefined) { + content = html``; + } else { + const items: [string, string][] = []; + + for (const helper of Object.keys(HELPERS)) { + items.push([ + helper, + this.hass.localize(`ui.panel.config.helpers.types.${helper}`) || + helper, + ]); + } + + for (const domain of this._helperFlows) { + items.push([domain, domainToName(this.hass.localize, domain)]); + } + + items.sort((a, b) => a[1].localeCompare(b[1])); + + content = html` + ${items.map(([domain, label]) => { + // Only OG helpers need to be loaded prior adding one + const isLoaded = + !(domain in HELPERS) || isComponentLoaded(this.hass, domain); + return html` + + + ${label} + + ${!isLoaded + ? html` + ${this.hass.localize( + "ui.dialogs.helper_settings.platform_not_loaded", + "platform", + domain + )} + ` + : ""} + `; + })} + + ${this.hass!.localize("ui.common.cancel")} + + `; + } + return html` - ${this._platform - ? html` -
    - ${this._error - ? html`
    ${this._error}
    ` - : ""} - ${dynamicElement(`ha-${this._platform}-form`, { - hass: this.hass, - item: this._item, - new: true, - })} -
    - - ${this.hass!.localize("ui.panel.config.helpers.dialog.create")} - - - ${this.hass!.localize("ui.common.back")} - - ` - : html` - ${Object.keys(HELPERS).map((platform: string) => { - const isLoaded = isComponentLoaded(this.hass, platform); - return html` - - - - ${this.hass.localize( - `ui.panel.config.helpers.types.${platform}` - ) || platform} - - - ${!isLoaded - ? html` - ${this.hass.localize( - "ui.dialogs.helper_settings.platform_not_loaded", - "platform", - platform - )} - ` - : ""} - `; - })} - - ${this.hass!.localize("ui.common.cancel")} - - `} + ${content}
    `; } @@ -160,13 +205,13 @@ export class DialogHelperDetail extends LitElement { } private async _createItem(): Promise { - if (!this._platform || !this._item) { + if (!this._domain || !this._item) { return; } this._submitting = true; this._error = ""; try { - await HELPERS[this._platform](this.hass, this._item); + await HELPERS[this._domain](this.hass, this._item); this.closeDialog(); } catch (err: any) { this._error = err.message || "Unknown error"; @@ -181,12 +226,22 @@ export class DialogHelperDetail extends LitElement { } ev.stopPropagation(); ev.preventDefault(); - this._platformPicked(ev); + this._domainPicked(ev); } - private _platformPicked(ev: Event): void { - this._platform = (ev.currentTarget! as any).platform; - this._focusForm(); + private _domainPicked(ev: Event): void { + const domain = (ev.currentTarget! as any).domain; + + if (domain in HELPERS) { + this._domain = domain; + this._focusForm(); + } else { + showConfigFlowDialog(this, { + startFlowHandler: domain, + dialogClosedCallback: this._params!.dialogClosedCallback, + }); + this.closeDialog(); + } } private async _focusForm(): Promise { @@ -195,7 +250,7 @@ export class DialogHelperDetail extends LitElement { } private _goBack() { - this._platform = undefined; + this._domain = undefined; this._item = undefined; this._error = undefined; } diff --git a/src/panels/config/helpers/forms/ha-timer-form.ts b/src/panels/config/helpers/forms/ha-timer-form.ts index 5d7b13cfad..5aef7ca212 100644 --- a/src/panels/config/helpers/forms/ha-timer-form.ts +++ b/src/panels/config/helpers/forms/ha-timer-form.ts @@ -21,16 +21,20 @@ class HaTimerForm extends LitElement { @state() private _duration!: string | number | DurationDict; + @state() private _restore!: boolean; + set item(item: Timer) { this._item = item; if (item) { this._name = item.name || ""; this._icon = item.icon || ""; this._duration = item.duration || "00:00:00"; + this._restore = item.restore || false; } else { this._name = ""; this._icon = ""; this._duration = "00:00:00"; + this._restore = false; } } @@ -79,6 +83,18 @@ class HaTimerForm extends LitElement { "ui.dialogs.helper_settings.timer.duration" )} > + + + +
    `; } @@ -104,6 +120,13 @@ class HaTimerForm extends LitElement { }); } + private _toggleRestore() { + this._restore = !this._restore; + fireEvent(this, "value-changed", { + value: { ...this._item, restore: this._restore }, + }); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index ac58f83e44..4173fd69ef 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -1,28 +1,66 @@ import { mdiPencilOff, mdiPlus } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; -import { HassEntity } from "home-assistant-js-websocket"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import memoize from "memoize-one"; +import memoizeOne from "memoize-one"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { domainIcon } from "../../../common/entity/domain_icon"; +import { LocalizeFunc } from "../../../common/translations/localize"; import { DataTableColumnContainer, RowClickedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/ha-fab"; +import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-icon"; import "../../../components/ha-svg-icon"; +import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; +import { + EntityRegistryEntry, + subscribeEntityRegistry, +} from "../../../data/entity_registry"; +import { domainToName } from "../../../data/integration"; import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage-data-table"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { HomeAssistant, Route } from "../../../types"; import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor"; import { configSections } from "../ha-panel-config"; import { HELPER_DOMAINS } from "./const"; import { showHelperDetailDialog } from "./show-dialog-helper-detail"; +import { navigate } from "../../../common/navigate"; +import { extractSearchParam } from "../../../common/url/search-params"; +import { getConfigFlowHandlers } from "../../../data/config_flow"; +import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; + +// This groups items by a key but only returns last entry per key. +const groupByOne = ( + items: T[], + keySelector: (item: T) => string +): Record => { + const result: Record = {}; + for (const item of items) { + result[keySelector(item)] = item; + } + return result; +}; + +const getConfigEntry = ( + entityEntries: Record, + configEntries: Record, + entityId: string +) => { + const configEntryId = entityEntries![entityId]?.config_entry_id; + return configEntryId ? configEntries![configEntryId] : undefined; +}; @customElement("ha-config-helpers") -export class HaConfigHelpers extends LitElement { +export class HaConfigHelpers extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property() public isWide!: boolean; @@ -33,98 +71,122 @@ export class HaConfigHelpers extends LitElement { @state() private _stateItems: HassEntity[] = []; - private _columns = memoize((narrow, _language): DataTableColumnContainer => { - const columns: DataTableColumnContainer = { - icon: { + @state() private _entityEntries?: Record; + + @state() private _configEntries?: Record; + + private _columns = memoizeOne( + (narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => { + const columns: DataTableColumnContainer = { + icon: { + title: "", + label: localize("ui.panel.config.helpers.picker.headers.icon"), + type: "icon", + template: (icon, helper: any) => + icon + ? html` ` + : html``, + }, + name: { + title: localize("ui.panel.config.helpers.picker.headers.name"), + sortable: true, + filterable: true, + grows: true, + direction: "asc", + template: (name, item: any) => + html` + ${name} + ${narrow + ? html`
    ${item.entity_id}
    ` + : ""} + `, + }, + }; + if (!narrow) { + columns.entity_id = { + title: localize("ui.panel.config.helpers.picker.headers.entity_id"), + sortable: true, + filterable: true, + width: "25%", + }; + } + columns.type = { + title: localize("ui.panel.config.helpers.picker.headers.type"), + sortable: true, + width: "25%", + filterable: true, + template: (type, row) => + row.configEntry + ? domainToName(localize, type) + : html` + ${localize(`ui.panel.config.helpers.types.${type}`) || type} + `, + }; + columns.editable = { title: "", label: this.hass.localize( - "ui.panel.config.helpers.picker.headers.icon" + "ui.panel.config.helpers.picker.headers.editable" ), type: "icon", - template: (icon, helper: any) => - icon - ? html` ` - : html``, - }, - name: { - title: this.hass.localize( - "ui.panel.config.helpers.picker.headers.name" - ), - sortable: true, - filterable: true, - grows: true, - direction: "asc", - template: (name, item: any) => - html` - ${name} - ${narrow - ? html`
    ${item.entity_id}
    ` - : ""} - `, - }, - }; - if (!narrow) { - columns.entity_id = { - title: this.hass.localize( - "ui.panel.config.helpers.picker.headers.entity_id" - ), - sortable: true, - filterable: true, - width: "25%", - }; - } - columns.type = { - title: this.hass.localize("ui.panel.config.helpers.picker.headers.type"), - sortable: true, - width: "25%", - filterable: true, - template: (type) => - html` - ${this.hass.localize(`ui.panel.config.helpers.types.${type}`) || type} + template: (editable) => html` + ${!editable + ? html` +
    + + + ${this.hass.localize( + "ui.panel.config.entities.picker.status.readonly" + )} + +
    + ` + : ""} `, - }; - columns.editable = { - title: "", - label: this.hass.localize( - "ui.panel.config.helpers.picker.headers.editable" - ), - type: "icon", - template: (editable) => html` - ${!editable - ? html` -
    - - - ${this.hass.localize( - "ui.panel.config.entities.picker.status.readonly" - )} - -
    - ` - : ""} - `, - }; - return columns; - }); + }; + return columns; + } + ); - private _getItems = memoize((stateItems: HassEntity[]) => - stateItems.map((entityState) => ({ - id: entityState.entity_id, - icon: entityState.attributes.icon, - name: entityState.attributes.friendly_name || "", - entity_id: entityState.entity_id, - editable: entityState.attributes.editable, - type: computeStateDomain(entityState), - })) + private _getItems = memoizeOne( + ( + stateItems: HassEntity[], + entityEntries: Record, + configEntries: Record + ) => + stateItems.map((entityState) => { + const configEntry = getConfigEntry( + entityEntries, + configEntries, + entityState.entity_id + ); + + return { + id: entityState.entity_id, + icon: entityState.attributes.icon, + name: entityState.attributes.friendly_name || "", + entity_id: entityState.entity_id, + editable: + configEntry !== undefined || entityState.attributes.editable, + type: configEntry + ? configEntry.domain + : computeStateDomain(entityState), + configEntry, + }; + }) ); protected render(): TemplateResult { - if (!this.hass || this._stateItems === undefined) { + if ( + !this.hass || + this._stateItems === undefined || + this._entityEntries === undefined || + this._configEntries === undefined + ) { return html` `; } @@ -135,8 +197,12 @@ export class HaConfigHelpers extends LitElement { back-path="/config" .route=${this.route} .tabs=${configSections.automations} - .columns=${this._columns(this.narrow, this.hass.language)} - .data=${this._getItems(this._stateItems)} + .columns=${this._columns(this.narrow, this.hass.localize)} + .data=${this._getItems( + this._stateItems, + this._entityEntries, + this._configEntries + )} @row-click=${this._openEditDialog} hasFab clickable @@ -160,32 +226,128 @@ export class HaConfigHelpers extends LitElement { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); - this._getStates(); - } - - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (oldHass && this._stateItems) { - this._getStates(oldHass); + this._getConfigEntries(); + if (this.route.path === "/add") { + this._handleAdd(); } } - private _getStates(oldHass?: HomeAssistant) { - let changed = false; - const tempStates = Object.values(this.hass!.states).filter((entity) => { - if (!HELPER_DOMAINS.includes(computeStateDomain(entity))) { - return false; + private async _handleAdd() { + const domain = extractSearchParam("domain"); + navigate("/config/helpers", { replace: true }); + if (!domain) { + return; + } + if (HELPER_DOMAINS.includes(domain)) { + showHelperDetailDialog(this, { + domain, + }); + return; + } + const handlers = await getConfigFlowHandlers(this.hass, "helper"); + + if (!handlers.includes(domain)) { + const integrations = await getConfigFlowHandlers( + this.hass, + "integration" + ); + if (integrations.includes(domain)) { + navigate(`/config/integrations/add?domain=${domain}`, { + replace: true, + }); + return; } - if (oldHass?.states[entity.entity_id] !== entity) { - changed = true; - } - return true; + 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" + ), + }); + return; + } + const localize = await this.hass.loadBackendTranslation( + "title", + domain, + true + ); + if ( + !(await showConfirmationDialog(this, { + title: this.hass.localize("ui.panel.config.integrations.confirm_new", { + integration: domainToName(localize, domain), + }), + })) + ) { + return; + } + showConfigFlowDialog(this, { + dialogClosedCallback: () => { + this._getConfigEntries(); + }, + startFlowHandler: domain, + showAdvanced: this.hass.userData?.showAdvanced, }); + } - if (changed || this._stateItems.length !== tempStates.length) { - this._stateItems = tempStates; + protected willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + + if (!this._entityEntries || !this._configEntries) { + return; } + + let changed = + !this._stateItems || + changedProps.has("_entityEntries") || + changedProps.has("_configEntries"); + + if (!changed && changedProps.has("hass")) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + changed = !oldHass || oldHass.states !== this.hass.states; + } + if (!changed) { + return; + } + + const extraEntities = new Set(); + + for (const entityEntry of Object.values(this._entityEntries)) { + if ( + entityEntry.config_entry_id && + entityEntry.config_entry_id in this._configEntries + ) { + extraEntities.add(entityEntry.entity_id); + } + } + + const newStates = Object.values(this.hass!.states).filter( + (entity) => + extraEntities.has(entity.entity_id) || + HELPER_DOMAINS.includes(computeStateDomain(entity)) + ); + + if ( + this._stateItems.length !== newStates.length || + !this._stateItems.every((val, idx) => newStates[idx] === val) + ) { + this._stateItems = newStates; + } + } + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeEntityRegistry(this.hass.connection!, (entries) => { + this._entityEntries = groupByOne(entries, (entry) => entry.entity_id); + }), + ]; + } + + private async _getConfigEntries() { + this._configEntries = groupByOne( + await getConfigEntries(this.hass, { type: "helper" }), + (entry) => entry.entry_id + ); } private async _openEditDialog(ev: CustomEvent): Promise { @@ -196,6 +358,12 @@ export class HaConfigHelpers extends LitElement { } private _createHelpler() { - showHelperDetailDialog(this); + showHelperDetailDialog(this, { + dialogClosedCallback: (params) => { + if (params.flowFinished) { + this._getConfigEntries(); + } + }, + }); } } diff --git a/src/panels/config/helpers/show-dialog-helper-detail.ts b/src/panels/config/helpers/show-dialog-helper-detail.ts index 959f92ad75..bbee0bc619 100644 --- a/src/panels/config/helpers/show-dialog-helper-detail.ts +++ b/src/panels/config/helpers/show-dialog-helper-detail.ts @@ -1,11 +1,21 @@ import { fireEvent } from "../../../common/dom/fire_event"; +import { DataEntryFlowDialogParams } from "../../../dialogs/config-flow/show-dialog-data-entry-flow"; export const loadHelperDetailDialog = () => import("./dialog-helper-detail"); -export const showHelperDetailDialog = (element: HTMLElement) => { +export interface ShowDialogHelperDetailParams { + domain?: string; + // Only used for config entries + dialogClosedCallback?: DataEntryFlowDialogParams["dialogClosedCallback"]; +} + +export const showHelperDetailDialog = ( + element: HTMLElement, + params: ShowDialogHelperDetailParams +) => { fireEvent(element, "show-dialog", { dialogTag: "dialog-helper-detail", dialogImport: loadHelperDetailDialog, - dialogParams: {}, + dialogParams: params, }); }; diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 5255d1197e..a78eb0e94b 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -67,6 +67,7 @@ import "./ha-ignored-config-entry-card"; import "./ha-integration-card"; import type { HaIntegrationCard } from "./ha-integration-card"; import { fetchDiagnosticHandlers } from "../../../data/diagnostics"; +import { HELPER_DOMAINS } from "../helpers/const"; export interface ConfigEntryUpdatedEvent { entry: ConfigEntry; @@ -521,24 +522,26 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { } private _loadConfigEntries() { - getConfigEntries(this.hass).then((configEntries) => { - this._configEntries = configEntries - .map( - (entry: ConfigEntry): ConfigEntryExtended => ({ - ...entry, - localized_domain_name: domainToName( - this.hass.localize, - entry.domain - ), - }) - ) - .sort((conf1, conf2) => - caseInsensitiveStringCompare( - conf1.localized_domain_name + conf1.title, - conf2.localized_domain_name + conf2.title + getConfigEntries(this.hass, { type: "integration" }).then( + (configEntries) => { + this._configEntries = configEntries + .map( + (entry: ConfigEntry): ConfigEntryExtended => ({ + ...entry, + localized_domain_name: domainToName( + this.hass.localize, + entry.domain + ), + }) ) - ); - }); + .sort((conf1, conf2) => + caseInsensitiveStringCompare( + conf1.localized_domain_name + conf1.title, + conf2.localized_domain_name + conf2.title + ) + ); + } + ); } private async _scanUSBDevices() { @@ -656,9 +659,22 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { if (!domain) { return; } - const handlers = await getConfigFlowHandlers(this.hass); + const handlers = await getConfigFlowHandlers(this.hass, "integration"); if (!handlers.includes(domain)) { + if (HELPER_DOMAINS.includes(domain)) { + navigate(`/config/helpers/add?domain=${domain}`, { + replace: true, + }); + 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" diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index 4af887d59a..28154e279c 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -55,8 +55,6 @@ const integrationsWithPanel = { hassio: "/hassio/dashboard", mqtt: "/config/mqtt", zha: "/config/zha/dashboard", - ozw: "/config/ozw/dashboard", - zwave: "/config/zwave", zwave_js: "/config/zwave_js/dashboard", }; diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts index 9337854c8e..eaa03dc98f 100644 --- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts @@ -111,7 +111,9 @@ class HaPanelDevMqtt extends LitElement { return; } const configEntryId = searchParams.get("config_entry") as string; - const configEntries = await getConfigEntries(this.hass); + const configEntries = await getConfigEntries(this.hass, { + domain: "mqtt", + }); const configEntry = configEntries.find( (entry) => entry.entry_id === configEntryId ); diff --git a/src/panels/config/integrations/integration-panels/ozw/dialog-ozw-refresh-node.ts b/src/panels/config/integrations/integration-panels/ozw/dialog-ozw-refresh-node.ts deleted file mode 100644 index 54ee3ce9d6..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/dialog-ozw-refresh-node.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/ha-circular-progress"; -import "../../../../../components/ha-code-editor"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { - fetchOZWNodeMetadata, - nodeQueryStages, - OZWDevice, - OZWDeviceMetaData, -} from "../../../../../data/ozw"; -import { haStyleDialog } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import { OZWRefreshNodeDialogParams } from "./show-dialog-ozw-refresh-node"; - -@customElement("dialog-ozw-refresh-node") -class DialogOZWRefreshNode extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _node_id?: number; - - @state() private _ozw_instance = 1; - - @state() private _nodeMetaData?: OZWDeviceMetaData; - - @state() private _node?: OZWDevice; - - @state() private _active = false; - - @state() private _complete = false; - - private _refreshDevicesTimeoutHandle?: number; - - private _subscribed?: Promise<() => Promise>; - - public disconnectedCallback(): void { - super.disconnectedCallback(); - this._unsubscribe(); - } - - protected updated(changedProperties: PropertyValues): void { - super.update(changedProperties); - if (changedProperties.has("node_id")) { - this._fetchData(); - } - } - - private async _fetchData() { - if (!this._node_id) { - return; - } - const metaDataResponse = await fetchOZWNodeMetadata( - this.hass, - this._ozw_instance, - this._node_id - ); - - this._nodeMetaData = metaDataResponse.metadata; - } - - public async showDialog(params: OZWRefreshNodeDialogParams): Promise { - this._node_id = params.node_id; - this._ozw_instance = params.ozw_instance; - this._fetchData(); - } - - protected render(): TemplateResult { - if (!this._node_id) { - return html``; - } - - return html` - - ${this._complete - ? html` -

    - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.complete" - )} -

    - - ${this.hass.localize("ui.common.close")} - - ` - : html` - ${this._active - ? html` -
    - -
    -

    - - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.refreshing_description" - )} - -

    - ${this._node - ? html` -

    - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.node_status" - )}: - ${this._node.node_query_stage} - (${this.hass.localize( - "ui.panel.config.ozw.refresh_node.step" - )} - ${nodeQueryStages.indexOf( - this._node.node_query_stage - ) + 1}/17) -

    -

    - - ${this.hass.localize( - "ui.panel.config.ozw.node_query_stages." + - this._node.node_query_stage.toLowerCase() - )} -

    - ` - : ``} -
    -
    - ` - : html` - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.description" - )} -

    - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.battery_note" - )} -

    - `} - ${this._nodeMetaData?.WakeupHelp !== "" - ? html` - - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.wakeup_header" - )} - ${this._nodeMetaData!.Name} - -
    - ${this._nodeMetaData!.WakeupHelp} -
    - - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.wakeup_instructions_source" - )} - -
    - ` - : ""} - ${!this._active - ? html` - - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.start_refresh_button" - )} - - ` - : html``} - `} -
    - `; - } - - private _startRefresh(): void { - this._subscribe(); - } - - private _handleMessage(message: any): void { - if (message.type === "node_updated") { - this._node = message; - if (message.node_query_stage === "Complete") { - this._unsubscribe(); - this._complete = true; - } - } - } - - private _unsubscribe(): void { - this._active = false; - if (this._refreshDevicesTimeoutHandle) { - clearTimeout(this._refreshDevicesTimeoutHandle); - } - if (this._subscribed) { - this._subscribed.then((unsub) => unsub()); - this._subscribed = undefined; - } - } - - private _subscribe(): void { - if (!this.hass) { - return; - } - this._active = true; - this._subscribed = this.hass.connection.subscribeMessage( - (message) => this._handleMessage(message), - { - type: "ozw/refresh_node_info", - node_id: this._node_id, - ozw_instance: this._ozw_instance, - } - ); - this._refreshDevicesTimeoutHandle = window.setTimeout( - () => this._unsubscribe(), - 120000 - ); - } - - private _close(): void { - this._complete = false; - this._node_id = undefined; - this._node = undefined; - } - - static get styles(): CSSResultGroup { - return [ - haStyleDialog, - css` - blockquote { - display: block; - background-color: #ddd; - padding: 8px; - margin: 8px 0; - font-size: 0.9em; - } - - blockquote em { - font-size: 0.9em; - margin-top: 6px; - } - - .flex-container { - display: flex; - align-items: center; - } - - .flex-container ha-circular-progress { - margin-right: 20px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-ozw-refresh-node": DialogOZWRefreshNode; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts deleted file mode 100644 index dcdb81518d..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts +++ /dev/null @@ -1,260 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { mdiCheckCircle, mdiCircle, mdiCloseCircle, mdiZWave } from "@mdi/js"; -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item-body"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { navigate } from "../../../../../common/navigate"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-next"; -import { - fetchOZWInstances, - networkOfflineStatuses, - networkOnlineStatuses, - networkStartingStatuses, - OZWInstance, -} from "../../../../../data/ozw"; -import "../../../../../layouts/hass-error-screen"; -import "../../../../../layouts/hass-loading-screen"; -import "../../../../../layouts/hass-tabs-subpage"; -import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import "../../../../../components/ha-alert"; - -export const ozwTabs: PageNavigation[] = []; - -@customElement("ozw-config-dashboard") -class OZWConfigDashboard extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @property() public configEntryId?: string; - - @state() private _instances?: OZWInstance[]; - - protected firstUpdated() { - this._fetchData(); - } - - protected render(): TemplateResult { - if (!this._instances) { - return html``; - } - - if (this._instances.length === 0) { - return html``; - } - - return html` - - - The OpenZWave integration is deprecated and will no longer receive any - updates. The technical dependencies will render this integration - unusable in the near future. We strongly advise you to migrate to the - new -
    Z-Wave JS integration. - - learn more - - - - -
    - ${this.hass.localize("ui.panel.config.ozw.select_instance.header")} -
    - -
    - ${this.hass.localize( - "ui.panel.config.ozw.select_instance.introduction" - )} -
    - ${this._instances.length > 0 - ? html` - ${this._instances.map((instance) => { - let status = "unknown"; - let icon = mdiCircle; - if (networkOnlineStatuses.includes(instance.Status)) { - status = "online"; - icon = mdiCheckCircle; - } - if (networkStartingStatuses.includes(instance.Status)) { - status = "starting"; - } - if (networkOfflineStatuses.includes(instance.Status)) { - status = "offline"; - icon = mdiCloseCircle; - } - - return html` - - - - - - - ${this.hass.localize( - "ui.panel.config.ozw.common.instance" - )} - ${instance.ozw_instance} -
    - - ${this.hass.localize( - "ui.panel.config.ozw.network_status." + status - )} - - - ${this.hass.localize( - "ui.panel.config.ozw.network_status.details." + - instance.Status.toLowerCase() - )}
    - ${this.hass.localize( - "ui.panel.config.ozw.common.controller" - )} - : ${instance.getControllerPath}
    - OZWDaemon ${instance.OZWDaemon_Version} (OpenZWave - ${instance.OpenZWave_Version}) -
    -
    - -
    -
    -
    - `; - })} - ` - : ""} -
    - - `; - } - - private async _fetchData() { - this._instances = await fetchOZWInstances(this.hass!); - if (this._instances.length === 1) { - navigate(`/config/ozw/network/${this._instances[0].ozw_instance}`, { - replace: true, - }); - } - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - ha-card:last-child { - margin-bottom: 24px; - } - ha-config-section { - margin-top: -12px; - } - :host([narrow]) ha-config-section { - margin-top: -20px; - } - ha-alert { - display: block; - margin: 16px; - } - ha-alert a { - text-decoration: none; - } - ha-card { - overflow: hidden; - } - ha-card a { - text-decoration: none; - color: var(--primary-text-color); - } - paper-item-body { - margin: 16px 0; - } - a { - text-decoration: none; - color: var(--primary-text-color); - position: relative; - display: block; - outline: 0; - } - ha-svg-icon.network-status-icon { - height: 14px; - width: 14px; - } - .online { - color: green; - } - .starting { - color: orange; - } - .offline { - color: red; - } - ha-svg-icon, - ha-icon-next { - color: var(--secondary-text-color); - } - .iron-selected paper-item::before, - a:not(.iron-selected):focus::before { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - pointer-events: none; - content: ""; - transition: opacity 15ms linear; - will-change: opacity; - } - a:not(.iron-selected):focus::before { - background-color: currentColor; - opacity: var(--dark-divider-opacity); - } - .iron-selected paper-item:focus::before, - .iron-selected:focus paper-item::before { - opacity: 0.2; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-config-dashboard": OZWConfigDashboard; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts deleted file mode 100644 index ad2d2e38fe..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { customElement, property } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { - HassRouterPage, - RouterOptions, -} from "../../../../../layouts/hass-router-page"; -import { HomeAssistant, Route } from "../../../../../types"; - -export const computeTail = memoizeOne((route: Route) => { - const dividerPos = route.path.indexOf("/", 1); - return dividerPos === -1 - ? { - prefix: route.prefix + route.path, - path: "", - } - : { - prefix: route.prefix + route.path.substr(0, dividerPos), - path: route.path.substr(dividerPos), - }; -}); - -@customElement("ozw-config-router") -class OZWConfigRouter extends HassRouterPage { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public isWide!: boolean; - - @property() public narrow!: boolean; - - private _configEntry = new URLSearchParams(window.location.search).get( - "config_entry" - ); - - protected routerOptions: RouterOptions = { - defaultPage: "dashboard", - showLoading: true, - routes: { - dashboard: { - tag: "ozw-config-dashboard", - load: () => import("./ozw-config-dashboard"), - }, - network: { - tag: "ozw-network-router", - load: () => import("./ozw-network-router"), - }, - }, - }; - - protected updatePageEl(el): void { - el.route = this.routeTail; - el.hass = this.hass; - el.isWide = this.isWide; - el.narrow = this.narrow; - el.configEntryId = this._configEntry; - if (this._currentPage === "network") { - const path = this.routeTail.path.split("/"); - el.ozwInstance = path[1]; - el.route = computeTail(this.routeTail); - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-config-router": OZWConfigRouter; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts deleted file mode 100644 index 7dc5478939..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts +++ /dev/null @@ -1,245 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { mdiCheckCircle, mdiCircle, mdiCloseCircle } from "@mdi/js"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; -import { navigate } from "../../../../../common/navigate"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import { - fetchOZWNetworkStatistics, - fetchOZWNetworkStatus, - networkOfflineStatuses, - networkOnlineStatuses, - networkStartingStatuses, - OZWInstance, - OZWNetworkStatistics, -} from "../../../../../data/ozw"; -import "../../../../../layouts/hass-tabs-subpage"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import { ozwNetworkTabs } from "./ozw-network-router"; - -@customElement("ozw-network-dashboard") -class OZWNetworkDashboard extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @property() public configEntryId?: string; - - @property() public ozwInstance?: number; - - @state() private _network?: OZWInstance; - - @state() private _statistics?: OZWNetworkStatistics; - - @state() private _status = "unknown"; - - @state() private _icon = mdiCircle; - - protected firstUpdated() { - if (!this.ozwInstance) { - navigate("/config/ozw/dashboard", { replace: true }); - } else if (this.hass) { - this._fetchData(); - } - } - - protected render(): TemplateResult { - return html` - - -
    - ${this.hass.localize("ui.panel.config.ozw.network.header")} -
    - -
    - ${this.hass.localize("ui.panel.config.ozw.network.introduction")} -
    - ${this._network - ? html` - -
    -
    - - ${this.hass.localize( - "ui.panel.config.ozw.common.network" - )} - ${this.hass.localize( - `ui.panel.config.ozw.network_status.${this._status}` - )} -
    - - ${this.hass.localize( - `ui.panel.config.ozw.network_status.details.${this._network.Status.toLowerCase()}` - )} - -
    -
    - ${this.hass.localize( - "ui.panel.config.ozw.common.ozw_instance" - )} - ${this._network.ozw_instance} - ${this._statistics - ? html` - • - ${this.hass.localize( - "ui.panel.config.ozw.network.node_count", - "count", - this._statistics.node_count - )} - ` - : ``} -
    - ${this.hass.localize( - "ui.panel.config.ozw.common.controller" - )}: - ${this._network.getControllerPath}
    - OZWDaemon ${this._network.OZWDaemon_Version} (OpenZWave - ${this._network.OpenZWave_Version}) -
    -
    -
    - ${this._generateServiceButton("add_node")} - ${this._generateServiceButton("remove_node")} - ${this._generateServiceButton("cancel_command")} -
    -
    - ` - : ``} -
    -
    - `; - } - - private async _fetchData() { - if (!this.ozwInstance) return; - this._network = await fetchOZWNetworkStatus(this.hass!, this.ozwInstance); - this._statistics = await fetchOZWNetworkStatistics( - this.hass!, - this.ozwInstance - ); - if (networkOnlineStatuses.includes(this._network!.Status)) { - this._status = "online"; - this._icon = mdiCheckCircle; - } - if (networkStartingStatuses.includes(this._network!.Status)) { - this._status = "starting"; - } - if (networkOfflineStatuses.includes(this._network!.Status)) { - this._status = "offline"; - this._icon = mdiCloseCircle; - } - } - - private _generateServiceButton(service: string) { - const serviceData = { instance_id: this.ozwInstance }; - return html` - - ${this.hass!.localize(`ui.panel.config.ozw.services.${service}`)} - - `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .secondary { - color: var(--secondary-text-color); - } - .online { - color: green; - } - .starting { - color: orange; - } - .offline { - color: red; - } - .content { - margin-top: 24px; - } - - .sectionHeader { - position: relative; - padding-right: 40px; - } - - .network-status { - text-align: center; - } - - .network-status div.details { - font-size: 1.5rem; - margin-bottom: 16px; - } - - .network-status ha-svg-icon { - display: block; - margin: 0px auto 16px; - width: 48px; - height: 48px; - } - - .network-status small { - font-size: 1rem; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - - .toggle-help-icon { - position: absolute; - top: -6px; - right: 0; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - padding: 0 8px 12px; - } - - [hidden] { - display: none; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-network-dashboard": OZWNetworkDashboard; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts deleted file mode 100644 index 8547058884..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts +++ /dev/null @@ -1,131 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { mdiAlert, mdiCheck } from "@mdi/js"; -import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { HASSDomEvent } from "../../../../../common/dom/fire_event"; -import { navigate } from "../../../../../common/navigate"; -import "../../../../../components/buttons/ha-call-service-button"; -import { - DataTableColumnContainer, - RowClickedEvent, -} from "../../../../../components/data-table/ha-data-table"; -import "../../../../../components/ha-card"; -import { fetchOZWNodes, OZWDevice } from "../../../../../data/ozw"; -import "../../../../../layouts/hass-tabs-subpage"; -import "../../../../../layouts/hass-tabs-subpage-data-table"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import { ozwNetworkTabs } from "./ozw-network-router"; - -export interface NodeRowData extends OZWDevice { - node?: NodeRowData; - id?: number; -} - -@customElement("ozw-network-nodes") -class OZWNetworkNodes extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @property() public configEntryId?: string; - - @property() public ozwInstance = 0; - - @state() private _nodes: OZWDevice[] = []; - - private _columns = memoizeOne( - (narrow: boolean): DataTableColumnContainer => ({ - node_id: { - title: this.hass.localize("ui.panel.config.ozw.nodes_table.id"), - sortable: true, - type: "numeric", - width: "72px", - filterable: true, - direction: "asc", - }, - node_product_name: { - title: this.hass.localize("ui.panel.config.ozw.nodes_table.model"), - sortable: true, - width: narrow ? "75%" : "25%", - }, - node_manufacturer_name: { - title: this.hass.localize( - "ui.panel.config.ozw.nodes_table.manufacturer" - ), - sortable: true, - hidden: narrow, - width: "25%", - }, - node_query_stage: { - title: this.hass.localize( - "ui.panel.config.ozw.nodes_table.query_stage" - ), - sortable: true, - width: narrow ? "25%" : "15%", - }, - is_zwave_plus: { - title: this.hass.localize("ui.panel.config.ozw.nodes_table.zwave_plus"), - hidden: narrow, - template: (value: boolean) => - value ? html` ` : "", - }, - is_failed: { - title: this.hass.localize("ui.panel.config.ozw.nodes_table.failed"), - hidden: narrow, - template: (value: boolean) => - value ? html` ` : "", - }, - }) - ); - - protected firstUpdated() { - if (!this.ozwInstance) { - navigate("/config/ozw/dashboard", { replace: true }); - } else if (this.hass) { - this._fetchData(); - } - } - - protected render(): TemplateResult { - return html` - - - `; - } - - private async _fetchData() { - this._nodes = await fetchOZWNodes(this.hass!, this.ozwInstance!); - } - - private _handleRowClicked(ev: HASSDomEvent) { - const nodeId = ev.detail.id; - navigate(`/config/ozw/network/${this.ozwInstance}/node/${nodeId}`); - } - - static get styles(): CSSResultGroup { - return haStyle; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-network-nodes": OZWNetworkNodes; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-network-router.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-router.ts deleted file mode 100644 index 0294ff9b0f..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-network-router.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { mdiNetwork, mdiServerNetwork } from "@mdi/js"; -import { customElement, property } from "lit/decorators"; -import { - HassRouterPage, - RouterOptions, -} from "../../../../../layouts/hass-router-page"; -import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; -import { HomeAssistant } from "../../../../../types"; -import { computeTail } from "./ozw-config-router"; - -export const ozwNetworkTabs = (instance: number): PageNavigation[] => [ - { - translationKey: "ui.panel.config.ozw.navigation.network", - path: `/config/ozw/network/${instance}/dashboard`, - iconPath: mdiServerNetwork, - }, - { - translationKey: "ui.panel.config.ozw.navigation.nodes", - path: `/config/ozw/network/${instance}/nodes`, - iconPath: mdiNetwork, - }, -]; - -@customElement("ozw-network-router") -class OZWNetworkRouter extends HassRouterPage { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public isWide!: boolean; - - @property() public narrow!: boolean; - - @property() public ozwInstance!: number; - - private _configEntry = new URLSearchParams(window.location.search).get( - "config_entry" - ); - - protected routerOptions: RouterOptions = { - defaultPage: "dashboard", - showLoading: true, - routes: { - dashboard: { - tag: "ozw-network-dashboard", - load: () => import("./ozw-network-dashboard"), - }, - nodes: { - tag: "ozw-network-nodes", - load: () => import("./ozw-network-nodes"), - }, - node: { - tag: "ozw-node-router", - load: () => import("./ozw-node-router"), - }, - }, - }; - - protected updatePageEl(el): void { - el.route = computeTail(this.routeTail); - el.hass = this.hass; - el.isWide = this.isWide; - el.narrow = this.narrow; - el.configEntryId = this._configEntry; - el.ozwInstance = this.ozwInstance; - if (this._currentPage === "node") { - el.nodeId = this.routeTail.path.split("/")[1]; - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-network-router": OZWNetworkRouter; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts deleted file mode 100644 index 7c29bf2cbc..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts +++ /dev/null @@ -1,265 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { navigate } from "../../../../../common/navigate"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import { - fetchOZWNodeConfig, - fetchOZWNodeMetadata, - fetchOZWNodeStatus, - OZWDevice, - OZWDeviceConfig, - OZWDeviceMetaDataResponse, -} from "../../../../../data/ozw"; -import { ERR_NOT_FOUND } from "../../../../../data/websocket_api"; -import "../../../../../layouts/hass-tabs-subpage"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import { ozwNodeTabs } from "./ozw-node-router"; -import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node"; - -@customElement("ozw-node-config") -class OZWNodeConfig extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @property() public configEntryId?: string; - - @property() public ozwInstance?; - - @property() public nodeId?; - - @state() private _node?: OZWDevice; - - @state() private _metadata?: OZWDeviceMetaDataResponse; - - @state() private _config?: OZWDeviceConfig[]; - - @state() private _error?: string; - - protected firstUpdated() { - if (!this.ozwInstance) { - navigate("/config/ozw/dashboard", { replace: true }); - } else if (!this.nodeId) { - navigate(`/config/ozw/network/${this.ozwInstance}/nodes`, { - replace: true, - }); - } else { - this._fetchData(); - } - } - - protected render(): TemplateResult { - if (this._error) { - return html` - - `; - } - - return html` - - -
    - ${this.hass.localize("ui.panel.config.ozw.node_config.header")} -
    - -
    - ${this.hass.localize( - "ui.panel.config.ozw.node_config.introduction" - )} -

    - - ${this.hass.localize( - "ui.panel.config.ozw.node_config.help_source" - )} - -

    -

    - Note: This panel is currently read-only. The ability to change - values will come in a later update. -

    -
    - ${this._node - ? html` - -
    - - ${this._node.node_manufacturer_name} - ${this._node.node_product_name}
    - ${this.hass.localize("ui.panel.config.ozw.common.node_id")}: - ${this._node.node_id}
    - ${this.hass.localize( - "ui.panel.config.ozw.common.query_stage" - )}: - ${this._node.node_query_stage} - ${this._metadata?.metadata.ProductManualURL - ? html` -

    - ${this.hass.localize( - "ui.panel.config.ozw.node_metadata.product_manual" - )} -

    -
    ` - : ``} -
    -
    - - ${this.hass.localize( - "ui.panel.config.ozw.refresh_node.button" - )} - -
    -
    - - ${this._metadata?.metadata.WakeupHelp - ? html` - -
    - - ${this.hass.localize( - "ui.panel.config.ozw.node_config.wakeup_help" - )} - -

    ${this._metadata.metadata.WakeupHelp}

    -
    -
    - ` - : ``} - ${this._config - ? html` - ${this._config.map( - (item) => html` - -
    - ${item.label}
    - ${item.help} -

    ${item.value}

    -
    -
    - ` - )} - ` - : ``} - ` - : ``} -
    -
    - `; - } - - private async _fetchData() { - if (!this.ozwInstance || !this.nodeId) { - return; - } - - try { - const nodeProm = fetchOZWNodeStatus( - this.hass!, - this.ozwInstance, - this.nodeId - ); - const metadataProm = fetchOZWNodeMetadata( - this.hass!, - this.ozwInstance, - this.nodeId - ); - const configProm = fetchOZWNodeConfig( - this.hass!, - this.ozwInstance, - this.nodeId - ); - [this._node, this._metadata, this._config] = await Promise.all([ - nodeProm, - metadataProm, - configProm, - ]); - } catch (err: any) { - if (err.code === ERR_NOT_FOUND) { - this._error = ERR_NOT_FOUND; - return; - } - throw err; - } - } - - private async _refreshNodeClicked() { - showOZWRefreshNodeDialog(this, { - node_id: this.nodeId, - ozw_instance: this.ozwInstance, - }); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .secondary { - color: var(--secondary-text-color); - font-size: 0.9em; - } - - .content { - margin-top: 24px; - } - - .sectionHeader { - position: relative; - padding-right: 40px; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - [hidden] { - display: none; - } - - blockquote { - display: block; - background-color: #ddd; - padding: 8px; - margin: 8px 0; - font-size: 0.9em; - } - - blockquote em { - font-size: 0.9em; - margin-top: 6px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-node-config": OZWNodeConfig; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts deleted file mode 100644 index ec45071c16..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts +++ /dev/null @@ -1,254 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { navigate } from "../../../../../common/navigate"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import { - fetchOZWNodeMetadata, - fetchOZWNodeStatus, - OZWDevice, - OZWDeviceMetaDataResponse, -} from "../../../../../data/ozw"; -import { ERR_NOT_FOUND } from "../../../../../data/websocket_api"; -import "../../../../../layouts/hass-tabs-subpage"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import { ozwNodeTabs } from "./ozw-node-router"; -import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node"; - -@customElement("ozw-node-dashboard") -class OZWNodeDashboard extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @property() public configEntryId?: string; - - @property() public ozwInstance?; - - @property() public nodeId?; - - @state() private _node?: OZWDevice; - - @state() private _metadata?: OZWDeviceMetaDataResponse; - - @state() private _not_found = false; - - protected firstUpdated() { - if (!this.ozwInstance) { - navigate("/config/ozw/dashboard", { replace: true }); - } else if (!this.nodeId) { - navigate(`/config/ozw/network/${this.ozwInstance}/nodes`, { - replace: true, - }); - } else if (this.hass) { - this._fetchData(); - } - } - - protected render(): TemplateResult { - if (this._not_found) { - return html` - - `; - } - - return html` - - -
    Node Management
    - -
    - View the status of a node and manage its configuration. -
    - ${this._node - ? html` - -
    -
    - - ${this._node.node_manufacturer_name} - ${this._node.node_product_name} - -
    - Node ID: ${this._node.node_id}
    - Query Stage: ${this._node.node_query_stage} - ${this._metadata?.metadata.ProductManualURL - ? html` -

    Product Manual

    -
    ` - : ``} -
    - ${this._metadata?.metadata.ProductPicBase64 - ? html`` - : ``} -
    -
    - - Refresh Node - -
    -
    - - ${this._metadata - ? html` - -
    - ${this._metadata.metadata.Description} -
    -
    - -
    - ${this._metadata.metadata.InclusionHelp} -
    -
    - -
    - ${this._metadata.metadata.ExclusionHelp} -
    -
    - -
    - ${this._metadata.metadata.ResetHelp} -
    -
    - ${this._metadata.metadata.WakeupHelp - ? html` - -
    - ${this._metadata.metadata.WakeupHelp} -
    -
    - ` - : ``} - ` - : ``} - ` - : ``} -
    -
    - `; - } - - private async _fetchData() { - if (!this.ozwInstance || !this.nodeId) { - return; - } - - try { - this._node = await fetchOZWNodeStatus( - this.hass!, - this.ozwInstance, - this.nodeId - ); - this._metadata = await fetchOZWNodeMetadata( - this.hass!, - this.ozwInstance, - this.nodeId - ); - } catch (err: any) { - if (err.code === ERR_NOT_FOUND) { - this._not_found = true; - return; - } - throw err; - } - } - - private async _refreshNodeClicked() { - showOZWRefreshNodeDialog(this, { - node_id: this.nodeId, - ozw_instance: this.ozwInstance, - }); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .secondary { - color: var(--secondary-text-color); - } - - .content { - margin-top: 24px; - } - - .content:last-child { - margin-bottom: 24px; - } - - .sectionHeader { - position: relative; - padding-right: 40px; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - .flex { - display: flex; - justify-content: space-between; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - - .toggle-help-icon { - position: absolute; - top: -6px; - right: 0; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - padding: 0 8px 12px; - } - - [hidden] { - display: none; - } - - .product-image { - padding: 12px; - max-height: 140px; - max-width: 140px; - } - .card-actions { - clear: right; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-node-dashboard": OZWNodeDashboard; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts deleted file mode 100644 index f5676bdcaa..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { mdiNetwork, mdiWrench } from "@mdi/js"; -import { customElement, property } from "lit/decorators"; -import { navigate } from "../../../../../common/navigate"; -import { - HassRouterPage, - RouterOptions, -} from "../../../../../layouts/hass-router-page"; -import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; -import { HomeAssistant } from "../../../../../types"; - -export const ozwNodeTabs = ( - instance: number, - node: number -): PageNavigation[] => [ - { - translationKey: "ui.panel.config.ozw.navigation.node.dashboard", - path: `/config/ozw/network/${instance}/node/${node}/dashboard`, - iconPath: mdiNetwork, - }, - { - translationKey: "ui.panel.config.ozw.navigation.node.config", - path: `/config/ozw/network/${instance}/node/${node}/config`, - iconPath: mdiWrench, - }, -]; - -@customElement("ozw-node-router") -class OZWNodeRouter extends HassRouterPage { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public isWide!: boolean; - - @property() public narrow!: boolean; - - @property() public ozwInstance!: number; - - @property() public nodeId!: number; - - private _configEntry = new URLSearchParams(window.location.search).get( - "config_entry" - ); - - protected routerOptions: RouterOptions = { - defaultPage: "dashboard", - showLoading: true, - routes: { - dashboard: { - tag: "ozw-node-dashboard", - load: () => import("./ozw-node-dashboard"), - }, - config: { - tag: "ozw-node-config", - load: () => import("./ozw-node-config"), - }, - }, - }; - - protected updatePageEl(el): void { - el.route = this.routeTail; - el.hass = this.hass; - el.isWide = this.isWide; - el.narrow = this.narrow; - el.configEntryId = this._configEntry; - el.ozwInstance = this.ozwInstance; - el.nodeId = this.nodeId; - - const searchParams = new URLSearchParams(window.location.search); - if (this._configEntry && !searchParams.has("config_entry")) { - searchParams.append("config_entry", this._configEntry); - navigate( - `${this.routeTail.prefix}${ - this.routeTail.path - }?${searchParams.toString()}`, - { replace: true } - ); - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "ozw-node-router": OZWNodeRouter; - } -} diff --git a/src/panels/config/integrations/integration-panels/ozw/show-dialog-ozw-refresh-node.ts b/src/panels/config/integrations/integration-panels/ozw/show-dialog-ozw-refresh-node.ts deleted file mode 100644 index e3ecd75e47..0000000000 --- a/src/panels/config/integrations/integration-panels/ozw/show-dialog-ozw-refresh-node.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; - -export interface OZWRefreshNodeDialogParams { - ozw_instance: number; - node_id: number; -} - -export const loadRefreshNodeDialog = () => import("./dialog-ozw-refresh-node"); - -export const showOZWRefreshNodeDialog = ( - element: HTMLElement, - refreshNodeDialogParams: OZWRefreshNodeDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-ozw-refresh-node", - dialogImport: loadRefreshNodeDialog, - dialogParams: refreshNodeDialogParams, - }); -}; diff --git a/src/panels/config/integrations/integration-panels/zwave/ha-config-zwave.js b/src/panels/config/integrations/integration-panels/zwave/ha-config-zwave.js deleted file mode 100644 index 58caa941af..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/ha-config-zwave.js +++ /dev/null @@ -1,765 +0,0 @@ -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { computeStateDomain } from "../../../../../common/entity/compute_state_domain"; -import { computeStateName } from "../../../../../common/entity/compute_state_name"; -import { sortStatesByName } from "../../../../../common/entity/states_sort_by_name"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-alert"; -import "../../../../../components/ha-icon"; -import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-icon-button-arrow-prev"; -import "../../../../../components/ha-menu-button"; -import "../../../../../components/ha-service-description"; -import "../../../../../layouts/ha-app-layout"; -import { EventsMixin } from "../../../../../mixins/events-mixin"; -import LocalizeMixin from "../../../../../mixins/localize-mixin"; -import "../../../../../styles/polymer-ha-style"; -import "../../../ha-config-section"; -import "../../../ha-form-style"; -import "./zwave-groups"; -import "./zwave-log"; -import "./zwave-network"; -import "./zwave-node-config"; -import "./zwave-node-protection"; -import "./zwave-usercodes"; -import "./zwave-values"; - -/* - * @appliesMixin LocalizeMixin - * @appliesMixin EventsMixin - */ -class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - - - - -
    [[localize('component.zwave.title')]]
    -
    -
    - - - This Z-Wave integration is deprecated and will no longer receive any - updates. The technical dependencies will render this integration - unusable in the near future. We strongly advise you to migrate to the - new - Z-Wave JS integration. - - learn more - - - - - -
    - [[localize('ui.panel.config.zwave.migration.zwave_js.introduction')]] -
    - -
    -
    - - - - - -
    - [[localize('ui.panel.config.zwave.node_management.header')]] - - - -
    - - [[localize('ui.panel.config.zwave.node_management.introduction')]] - - - -
    - - - - - -
    - - - -
    - - - - - - - - -
    - - - -
    - `; - } - - static get properties() { - return { - hass: Object, - - isWide: Boolean, - - nodes: { - type: Array, - computed: "computeNodes(hass)", - }, - - selectedNode: { - type: Number, - value: -1, - observer: "selectedNodeChanged", - }, - - nodeFailed: { - type: Boolean, - value: false, - }, - - config: { - type: Array, - value: () => [], - }, - - entities: { - type: Array, - computed: "computeEntities(selectedNode)", - }, - - selectedEntity: { - type: Number, - value: -1, - observer: "selectedEntityChanged", - }, - - values: { - type: Array, - }, - - groups: { - type: Array, - }, - - userCodes: { - type: Array, - value: () => [], - }, - - hasNodeUserCodes: { - type: Boolean, - value: false, - }, - - showHelp: { - type: Boolean, - value: false, - }, - - entityIgnored: Boolean, - - entityPollingIntensity: { - type: Number, - value: 0, - }, - - _protection: { - type: Array, - value: () => [], - }, - - _protectionNode: { - type: Boolean, - value: false, - }, - }; - } - - ready() { - super.ready(); - import("web-animations-js/web-animations-next-lite.min"); - this.addEventListener("hass-service-called", (ev) => - this.serviceCalled(ev) - ); - } - - attached() { - setCancelSyntheticClickEvents(true); - } - - detached() { - setCancelSyntheticClickEvents(false); - } - - serviceCalled(ev) { - if (ev.detail.success && ev.detail.service === "set_poll_intensity") { - this._saveEntity(); - } - } - - computeNodes(hass) { - return Object.keys(hass.states) - .map((key) => hass.states[key]) - .filter((ent) => ent.entity_id.match("zwave[.]")) - .sort(sortStatesByName); - } - - computeEntities(selectedNode) { - if (!this.nodes || selectedNode === -1) { - return -1; - } - const nodeid = this.nodes[this.selectedNode].attributes.node_id; - const hass = this.hass; - return Object.keys(this.hass.states) - .map((key) => hass.states[key]) - .filter((ent) => { - if (ent.attributes.node_id === undefined) { - return false; - } - return ( - "node_id" in ent.attributes && - ent.attributes.node_id === nodeid && - !ent.entity_id.match("zwave[.]") - ); - }) - .sort(sortStatesByName); - } - - selectedNodeChanged(selectedNode) { - if (selectedNode === -1) { - return; - } - this.selectedEntity = -1; - - this.hass - .callApi( - "GET", - `zwave/config/${this.nodes[selectedNode].attributes.node_id}` - ) - .then((configs) => { - this.config = this._objToArray(configs); - }); - - this.hass - .callApi( - "GET", - `zwave/values/${this.nodes[selectedNode].attributes.node_id}` - ) - .then((values) => { - this.values = this._objToArray(values); - }); - - this.hass - .callApi( - "GET", - `zwave/groups/${this.nodes[selectedNode].attributes.node_id}` - ) - .then((groups) => { - this.groups = this._objToArray(groups); - }); - - this.hasNodeUserCodes = false; - this.notifyPath("hasNodeUserCodes"); - this.hass - .callApi( - "GET", - `zwave/usercodes/${this.nodes[selectedNode].attributes.node_id}` - ) - .then((usercodes) => { - this.userCodes = this._objToArray(usercodes); - this.hasNodeUserCodes = this.userCodes.length > 0; - this.notifyPath("hasNodeUserCodes"); - }); - this.hass - .callApi( - "GET", - `zwave/protection/${this.nodes[selectedNode].attributes.node_id}` - ) - .then((protections) => { - this._protection = this._objToArray(protections); - if (this._protection) { - if (this._protection.length === 0) { - return; - } - this._protectionNode = true; - } - }); - - this.nodeFailed = this.nodes[selectedNode].attributes.is_failed; - } - - selectedEntityChanged(selectedEntity) { - if (selectedEntity === -1) { - return; - } - this.hass - .callApi( - "GET", - `zwave/values/${this.nodes[this.selectedNode].attributes.node_id}` - ) - .then((values) => { - this.values = this._objToArray(values); - }); - - const valueId = this.entities[selectedEntity].attributes.value_id; - const valueData = this.values.find((obj) => obj.key === valueId); - const valueIndex = this.values.indexOf(valueData); - this.hass - .callApi( - "GET", - `config/zwave/device_config/${this.entities[selectedEntity].entity_id}` - ) - .then((data) => { - this.setProperties({ - entityIgnored: data.ignored || false, - entityPollingIntensity: this.values[valueIndex].value.poll_intensity, - }); - }) - .catch(() => { - this.setProperties({ - entityIgnored: false, - entityPollingIntensity: this.values[valueIndex].value.poll_intensity, - }); - }); - } - - computeSelectCaption(stateObj) { - return ( - computeStateName(stateObj) + - " (Node:" + - stateObj.attributes.node_id + - " " + - stateObj.attributes.query_stage + - ")" - ); - } - - computeSelectCaptionEnt(stateObj) { - return computeStateDomain(stateObj) + "." + computeStateName(stateObj); - } - - computeIsNodeSelected() { - return this.nodes && this.selectedNode !== -1; - } - - computeIsEntitySelected(selectedEntity) { - return selectedEntity === -1; - } - - computeNodeServiceData(selectedNode) { - return { node_id: this.nodes[selectedNode].attributes.node_id }; - } - - computeHealNodeServiceData(selectedNode) { - return { - node_id: this.nodes[selectedNode].attributes.node_id, - return_routes: true, - }; - } - - computeRefreshEntityServiceData(selectedEntity) { - if (selectedEntity === -1) { - return -1; - } - return { entity_id: this.entities[selectedEntity].entity_id }; - } - - computePollIntensityServiceData(entityPollingIntensity) { - if (this.selectedNode === -1 || this.selectedEntity === -1) { - return -1; - } - return { - node_id: this.nodes[this.selectedNode].attributes.node_id, - value_id: this.entities[this.selectedEntity].attributes.value_id, - poll_intensity: parseInt(entityPollingIntensity), - }; - } - - _nodeMoreInfo() { - this.fire("hass-more-info", { - entityId: this.nodes[this.selectedNode].entity_id, - }); - } - - _entityMoreInfo() { - this.fire("hass-more-info", { - entityId: this.entities[this.selectedEntity].entity_id, - }); - } - - _saveEntity() { - const data = { - ignored: this.entityIgnored, - polling_intensity: parseInt(this.entityPollingIntensity), - }; - return this.hass.callApi( - "POST", - `config/zwave/device_config/${ - this.entities[this.selectedEntity].entity_id - }`, - data - ); - } - - toggleHelp() { - this.showHelp = !this.showHelp; - } - - _objToArray(obj) { - const array = []; - Object.keys(obj).forEach((key) => { - array.push({ - key, - value: obj[key], - }); - }); - return array; - } - - _backTapped() { - history.back(); - } - - entityIgnoredChanged(ev) { - this.entityIgnored = ev.target.checked; - } -} - -customElements.define("ha-config-zwave", HaConfigZwave); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-config-router.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-config-router.ts deleted file mode 100644 index 4ebb4b9970..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-config-router.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { customElement, property } from "lit/decorators"; -import { navigate } from "../../../../../common/navigate"; -import { - HassRouterPage, - RouterOptions, -} from "../../../../../layouts/hass-router-page"; -import { HomeAssistant } from "../../../../../types"; - -@customElement("zwave-config-router") -class ZWaveConfigRouter extends HassRouterPage { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public isWide!: boolean; - - @property() public narrow!: boolean; - - private _configEntry = new URLSearchParams(window.location.search).get( - "config_entry" - ); - - protected routerOptions: RouterOptions = { - defaultPage: "dashboard", - showLoading: true, - routes: { - dashboard: { - tag: "ha-config-zwave", - load: () => - import(/* webpackChunkName: "ha-config-zwave" */ "./ha-config-zwave"), - }, - migration: { - tag: "zwave-migration", - load: () => - import(/* webpackChunkName: "zwave-migration" */ "./zwave-migration"), - }, - }, - }; - - protected updatePageEl(el): void { - el.route = this.routeTail; - el.hass = this.hass; - el.isWide = this.isWide; - el.narrow = this.narrow; - el.configEntryId = this._configEntry; - - const searchParams = new URLSearchParams(window.location.search); - if (this._configEntry && !searchParams.has("config_entry")) { - searchParams.append("config_entry", this._configEntry); - navigate( - `${this.routeTail.prefix}${ - this.routeTail.path - }?${searchParams.toString()}`, - { replace: true } - ); - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "zwave-config-router": ZWaveConfigRouter; - } -} diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-groups.js b/src/panels/config/integrations/integration-panels/zwave/zwave-groups.js deleted file mode 100644 index 129587fcfb..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-groups.js +++ /dev/null @@ -1,380 +0,0 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { computeStateName } from "../../../../../common/entity/compute_state_name"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import LocalizeMixin from "../../../../../mixins/localize-mixin"; -import "../../../../../styles/polymer-ha-style"; - -class ZwaveGroups extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - - -
    - - - - - -
    - - - -
    - `; - } - - static get properties() { - return { - hass: Object, - - nodes: Array, - - groups: Array, - - selectedNode: { - type: Number, - observer: "_selectedNodeChanged", - }, - - _selectedTargetNode: { - type: Number, - value: -1, - observer: "_selectedTargetNodeChanged", - }, - - _selectedGroup: { - type: Number, - value: -1, - }, - - _otherGroupNodes: { - type: Array, - value: -1, - computed: "_computeOtherGroupNodes(_selectedGroup)", - }, - - _maxAssociations: { - type: String, - value: "", - computed: "_computeMaxAssociations(_selectedGroup)", - }, - - _noAssociationsLeft: { - type: Boolean, - value: true, - computed: "_computeAssociationsLeft(_selectedGroup)", - }, - - _addAssocServiceData: { - type: String, - value: "", - }, - - _removeAssocServiceData: { - type: String, - value: "", - }, - - _removeBroadcastNodeServiceData: { - type: String, - value: "", - }, - - _isBroadcastNodeInGroup: { - type: Boolean, - value: false, - }, - }; - } - - static get observers() { - return ["_selectedGroupChanged(groups, _selectedGroup)"]; - } - - ready() { - super.ready(); - this.addEventListener("hass-service-called", (ev) => - this.serviceCalled(ev) - ); - } - - serviceCalled(ev) { - if (ev.detail.success) { - setTimeout(() => { - this._refreshGroups(this.selectedNode); - }, 5000); - } - } - - _computeAssociationsLeft(selectedGroup) { - if (selectedGroup === -1) return true; - return this._maxAssociations === this._otherGroupNodes.length; - } - - _computeMaxAssociations(selectedGroup) { - if (selectedGroup === -1) return -1; - const maxAssociations = this.groups[selectedGroup].value.max_associations; - if (!maxAssociations) return "None"; - return maxAssociations; - } - - _computeOtherGroupNodes(selectedGroup) { - if (selectedGroup === -1) return -1; - this.setProperties({ _isBroadcastNodeInGroup: false }); - const associations = Object.values( - this.groups[selectedGroup].value.association_instances - ); - if (!associations.length) return ["None"]; - return associations.map((assoc) => { - if (!assoc.length || assoc.length !== 2) { - return `Unknown Node: ${assoc}`; - } - const id = assoc[0]; - const instance = assoc[1]; - const node = this.nodes.find((n) => n.attributes.node_id === id); - if (id === 255) { - this.setProperties({ - _isBroadcastNodeInGroup: true, - _removeBroadcastNodeServiceData: { - node_id: this.nodes[this.selectedNode].attributes.node_id, - association: "remove", - target_node_id: 255, - group: this.groups[selectedGroup].key, - }, - }); - } - if (!node) { - return `Unknown Node (${id}: (${instance} ? ${id}.${instance} : ${id}))`; - } - let caption = this._computeSelectCaption(node); - if (instance) { - caption += `/ Instance: ${instance}`; - } - return caption; - }); - } - - _computeTargetInGroup(selectedGroup, selectedTargetNode) { - if (selectedGroup === -1 || selectedTargetNode === -1) return false; - const associations = Object.values( - this.groups[selectedGroup].value.associations - ); - if (!associations.length) return false; - return ( - associations.indexOf( - this.nodes[selectedTargetNode].attributes.node_id - ) !== -1 - ); - } - - _computeSelectCaption(stateObj) { - return `${computeStateName(stateObj)} - (Node: ${stateObj.attributes.node_id} - ${stateObj.attributes.query_stage})`; - } - - _computeSelectCaptionGroup(stateObj) { - return `${stateObj.key}: ${stateObj.value.label}`; - } - - _computeIsTargetNodeSelected(selectedTargetNode) { - return this.nodes && selectedTargetNode !== -1; - } - - _computeIsGroupSelected(selectedGroup) { - return this.nodes && this.selectedNode !== -1 && selectedGroup !== -1; - } - - _computeAssocServiceData(selectedGroup, type) { - if ( - !this.groups || - selectedGroup === -1 || - this.selectedNode === -1 || - this._selectedTargetNode === -1 - ) { - return -1; - } - return { - node_id: this.nodes[this.selectedNode].attributes.node_id, - association: type, - target_node_id: this.nodes[this._selectedTargetNode].attributes.node_id, - group: this.groups[selectedGroup].key, - }; - } - - async _refreshGroups(selectedNode) { - const groupData = []; - const groups = await this.hass.callApi( - "GET", - `zwave/groups/${this.nodes[selectedNode].attributes.node_id}` - ); - Object.keys(groups).forEach((key) => { - groupData.push({ - key, - value: groups[key], - }); - }); - this.setProperties({ - groups: groupData, - _maxAssociations: groupData[this._selectedGroup].value.max_associations, - _otherGroupNodes: Object.values( - groupData[this._selectedGroup].value.associations - ), - _isBroadcastNodeInGroup: false, - }); - const oldGroup = this._selectedGroup; - this.setProperties({ _selectedGroup: -1 }); - this.setProperties({ _selectedGroup: oldGroup }); - } - - _selectedGroupChanged() { - if (this._selectedGroup === -1) return; - this.setProperties({ - _maxAssociations: this.groups[this._selectedGroup].value.max_associations, - _otherGroupNodes: Object.values( - this.groups[this._selectedGroup].value.associations - ), - }); - } - - _selectedTargetNodeChanged() { - if (this._selectedGroup === -1) return; - if ( - this._computeTargetInGroup(this._selectedGroup, this._selectedTargetNode) - ) { - this.setProperties({ - _removeAssocServiceData: this._computeAssocServiceData( - this._selectedGroup, - "remove" - ), - }); - } else { - this.setProperties({ - _addAssocServiceData: this._computeAssocServiceData( - this._selectedGroup, - "add" - ), - }); - } - } - - _selectedNodeChanged() { - if (this.selectedNode === -1) return; - this.setProperties({ _selectedTargetNode: -1, _selectedGroup: -1 }); - } -} - -customElements.define("zwave-groups", ZwaveGroups); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-log-dialog.js b/src/panels/config/integrations/integration-panels/zwave/zwave-log-dialog.js deleted file mode 100644 index df5acc95d1..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-log-dialog.js +++ /dev/null @@ -1,83 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { EventsMixin } from "../../../../../mixins/events-mixin"; -import "../../../../../styles/polymer-ha-style-dialog"; -import "../../../../../components/ha-dialog"; - -class ZwaveLogDialog extends EventsMixin(PolymerElement) { - static get template() { - return html` - - -
    -
    [[_ozwLog]]
    -
    - - `; - } - - static get properties() { - return { - hass: Object, - _ozwLog: String, - - _dialogClosedCallback: Function, - - _opened: { - type: Boolean, - value: false, - }, - - _intervalId: String, - - _numLogLines: { - type: Number, - }, - }; - } - - ready() { - super.ready(); - this.addEventListener("iron-overlay-closed", (ev) => - this._dialogClosed(ev) - ); - } - - showDialog({ _ozwLog, hass, _tail, _numLogLines, dialogClosedCallback }) { - this.hass = hass; - this._ozwLog = _ozwLog; - this._opened = true; - this._dialogClosedCallback = dialogClosedCallback; - this._numLogLines = _numLogLines; - if (_tail) { - this.setProperties({ - _intervalId: setInterval(() => { - this._refreshLog(); - }, 1500), - }); - } - } - - closeDialog() { - clearInterval(this._intervalId); - this._opened = false; - const closedEvent = true; - this._dialogClosedCallback({ closedEvent }); - this._dialogClosedCallback = null; - } - - async _refreshLog() { - const info = await this.hass.callApi( - "GET", - "zwave/ozwlog?lines=" + this._numLogLines - ); - this.setProperties({ _ozwLog: info }); - } -} - -customElements.define("zwave-log-dialog", ZwaveLogDialog); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-log.js b/src/panels/config/integrations/integration-panels/zwave/zwave-log.js deleted file mode 100755 index c55e153f00..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-log.js +++ /dev/null @@ -1,160 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import isPwa from "../../../../../common/config/is_pwa"; -import "../../../../../components/ha-card"; -import { EventsMixin } from "../../../../../mixins/events-mixin"; -import LocalizeMixin from "../../../../../mixins/localize-mixin"; -import "../../../../../styles/polymer-ha-style"; -import "../../../ha-config-section"; - -let registeredDialog = false; - -class OzwLog extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - - - [[localize('ui.panel.config.zwave.ozw_log.header')]] - - - [[localize('ui.panel.config.zwave.ozw_log.introduction')]] - - -
    - - -
    -
    - [[localize('ui.panel.config.zwave.ozw_log.load')]] - [[localize('ui.panel.config.zwave.ozw_log.tail')]] - - -`; - } - - static get properties() { - return { - hass: Object, - - isWide: { - type: Boolean, - value: false, - }, - - _ozwLogs: String, - - _completeLog: { - type: Boolean, - value: true, - }, - - numLogLines: { - type: Number, - value: 0, - observer: "_isCompleteLog", - }, - - _intervalId: String, - - tail: Boolean, - }; - } - - async _tailLog() { - this.setProperties({ tail: true }); - const ozwWindow = await this._openLogWindow(); - if (!isPwa()) { - this.setProperties({ - _intervalId: setInterval(() => { - this._refreshLog(ozwWindow); - }, 1500), - }); - } - } - - async _openLogWindow() { - const info = await this.hass.callApi( - "GET", - "zwave/ozwlog?lines=" + this.numLogLines - ); - this.setProperties({ _ozwLogs: info }); - if (isPwa()) { - this._showOzwlogDialog(); - return -1; - } - const ozwWindow = open("", "ozwLog", "toolbar"); - ozwWindow.document.body.innerHTML = `
    ${this._ozwLogs}
    `; - return ozwWindow; - } - - async _refreshLog(ozwWindow) { - if (ozwWindow.closed === true) { - clearInterval(this._intervalId); - this.setProperties({ _intervalId: null }); - } else { - const info = await this.hass.callApi( - "GET", - "zwave/ozwlog?lines=" + this.numLogLines - ); - this.setProperties({ _ozwLogs: info }); - ozwWindow.document.body.innerHTML = `
    ${this._ozwLogs}
    `; - } - } - - _isCompleteLog() { - if (this.numLogLines !== "0") { - this.setProperties({ _completeLog: false }); - } else { - this.setProperties({ _completeLog: true }); - } - } - - connectedCallback() { - super.connectedCallback(); - if (!registeredDialog) { - registeredDialog = true; - this.fire("register-dialog", { - dialogShowEvent: "show-ozwlog-dialog", - dialogTag: "zwave-log-dialog", - dialogImport: () => import("./zwave-log-dialog"), - }); - } - } - - _showOzwlogDialog() { - this.fire("show-ozwlog-dialog", { - hass: this.hass, - _numLogLines: this.numLogLines, - _ozwLog: this._ozwLogs, - _tail: this.tail, - dialogClosedCallback: () => this._dialogClosed(), - }); - } - - _dialogClosed() { - this.setProperties({ - tail: false, - }); - } -} -customElements.define("ozw-log", OzwLog); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts deleted file mode 100644 index 2272bd884e..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts +++ /dev/null @@ -1,573 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; -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 { computeStateDomain } from "../../../../../common/entity/compute_state_domain"; -import { computeStateName } from "../../../../../common/entity/compute_state_name"; -import "../../../../../components/buttons/ha-call-api-button"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-alert"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-circular-progress"; -import "../../../../../components/ha-icon"; -import "../../../../../components/ha-icon-button"; -import { - computeDeviceName, - DeviceRegistryEntry, - fetchDeviceRegistry, - subscribeDeviceRegistry, -} from "../../../../../data/device_registry"; -import { - fetchMigrationConfig, - fetchNetworkStatus, - startZwaveJsConfigFlow, - ZWaveMigrationConfig, - ZWaveNetworkStatus, - ZWAVE_NETWORK_STATE_STOPPED, -} from "../../../../../data/zwave"; -import { - fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus, - fetchZwaveNodeStatus, - getZwaveJsIdentifiersFromDevice, - migrateZwave, - subscribeZwaveNodeReady, - ZWaveJsMigrationData, -} from "../../../../../data/zwave_js"; -import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow"; -import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"; -import "../../../../../layouts/hass-subpage"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; - -@customElement("zwave-migration") -export class ZwaveMigration extends LitElement { - @property({ type: Object }) public hass!: HomeAssistant; - - @property({ type: Object }) public route!: Route; - - @property({ type: Boolean }) public narrow!: boolean; - - @property({ type: Boolean }) public isWide!: boolean; - - @state() private _networkStatus?: ZWaveNetworkStatus; - - @state() private _step = 0; - - @state() private _stoppingNetwork = false; - - @state() private _migrationConfig?: ZWaveMigrationConfig; - - @state() private _migrationData?: ZWaveJsMigrationData; - - @state() private _migratedZwaveEntities?: string[]; - - @state() private _deviceNameLookup: { [id: string]: string } = {}; - - @state() private _waitingOnDevices?: DeviceRegistryEntry[]; - - private _zwaveJsEntryId?: string; - - private _nodeReadySubscriptions?: Promise[]; - - private _unsub?: Promise; - - private _unsubDevices?: UnsubscribeFunc; - - public disconnectedCallback(): void { - this._unsubscribe(); - if (this._unsubDevices) { - this._unsubDevices(); - this._unsubDevices = undefined; - } - } - - protected render(): TemplateResult { - return html` - - -
    - ${this.hass.localize( - "ui.panel.config.zwave.migration.zwave_js.header" - )} -
    - -
    - ${this.hass.localize( - "ui.panel.config.zwave.migration.zwave_js.introduction" - )} -
    - ${html` - ${this._step === 0 - ? html` - -
    -

    - This wizard will walk through the following steps to - migrate from the legacy Z-Wave integration to Z-Wave JS. -

    -
      -
    1. Stop the Z-Wave network
    2. - ${!isComponentLoaded(this.hass, "hassio") - ? html`
    3. Configure and start Z-Wave JS
    4. ` - : ""} -
    5. Set up the Z-Wave JS integration
    6. -
    7. - Migrate entities and devices to the new integration -
    8. -
    9. Remove legacy Z-Wave integration
    10. -
    -

    - - ${isComponentLoaded(this.hass, "hassio") - ? html`Please - make a backup - before proceeding.` - : "Please make a backup of your installation before proceeding."} - -

    -
    -
    - - Continue - -
    -
    - ` - : this._step === 1 - ? html` - -
    -

    - We need to stop the Z-Wave network to perform the - migration. Home Assistant will not be able to control - Z-Wave devices while the network is stopped. -

    - ${Object.values(this.hass.states) - .filter( - (entityState) => - computeStateDomain(entityState) === "zwave" && - !["ready", "sleeping"].includes(entityState.state) - ) - .map( - (entityState) => - html` - Device ${computeStateName(entityState)} - (${entityState.entity_id}) is not ready yet! For - the best result, wake the device up if it is - battery powered and wait for this device to become - ready. - ` - )} - ${this._stoppingNetwork - ? html` -
    - -

    Stopping Z-Wave Network...

    -
    - ` - : ``} -
    -
    - - Stop Network - -
    -
    - ` - : this._step === 2 - ? html` - -
    -

    Now it's time to set up the Z-Wave JS integration.

    - ${isComponentLoaded(this.hass, "hassio") - ? html` -

    - Z-Wave JS runs as a Home Assistant add-on that - will be setup next. Make sure to check the - checkbox to use the add-on. -

    - ` - : html` -

    - You are not running Home Assistant OS (the default - installation type) or Home Assistant Supervised, - so we can not setup Z-Wave JS automaticaly. Follow - the - advanced installation instructions - to install Z-Wave JS. -

    -

    - Here's the current Z-Wave configuration. You'll - need these values when setting up Z-Wave JS. -

    - ${this._migrationConfig - ? html`
    - USB Path: ${this._migrationConfig.usb_path}
    - Network Key: - ${this._migrationConfig.network_key} -
    ` - : ``} -

    - Once Z-Wave JS is installed and running, click - 'Continue' to set up the Z-Wave JS integration and - migrate your devices and entities. -

    - `} -
    -
    - - Continue - -
    -
    - ` - : this._step === 3 - ? html` - -
    -

    - Now it's time to migrate your devices and entities from - the legacy Z-Wave integration to the Z-Wave JS - integration, to make sure all your UI's and automations - keep working. -

    - ${this._waitingOnDevices?.map( - (device) => - html` - Device ${computeDeviceName(device, this.hass)} is - not ready yet! For the best result, wake the device - up if it is battery powered and wait for this device - to become ready. - ` - )} - ${this._migrationData - ? html` -

    Below is a list of what will be migrated.

    - ${this._migratedZwaveEntities!.length !== - this._migrationData.zwave_entity_ids.length - ? html` - The following entities will not be migrated - and might need manual adjustments to your - config: - -
      - ${this._migrationData.zwave_entity_ids.map( - (entity_id) => - !this._migratedZwaveEntities!.includes( - entity_id - ) - ? html`
    • - ${entity_id in this.hass.states - ? computeStateName( - this.hass.states[entity_id] - ) - : ""} - (${entity_id}) -
    • ` - : "" - )} -
    ` - : ""} - ${Object.keys( - this._migrationData.migration_device_map - ).length - ? html`

    Devices that will be migrated:

    -
      - ${Object.keys( - this._migrationData.migration_device_map - ).map( - (device_id) => - html`
    • - ${this._deviceNameLookup[device_id] || - device_id} -
    • ` - )} -
    ` - : ""} - ${Object.keys( - this._migrationData.migration_entity_map - ).length - ? html`

    Entities that will be migrated:

    -
      - ${Object.keys( - this._migrationData.migration_entity_map - ).map( - (entity_id) => html`
    • - ${entity_id in this.hass.states - ? computeStateName( - this.hass.states[entity_id] - ) - : ""} - (${entity_id}) -
    • ` - )} -
    ` - : ""} - ` - : html`
    -

    Loading migration data...

    - - -
    `} -
    -
    - - Migrate - -
    -
    - ` - : this._step === 4 - ? html` -
    - That was all! You are now migrated to the new Z-Wave JS - integration, check if all your devices and entities are back - the way they where, if not all entities could be migrated - you might have to change those manually. -

    - If you have 'zwave' in your configurtion.yaml file, you - should remove it now. -

    -
    - -
    ` - : ""} - `} -
    -
    - `; - } - - private async _getMigrationConfig(): Promise { - this._migrationConfig = await fetchMigrationConfig(this.hass!); - } - - private async _unsubscribe(): Promise { - if (this._unsub) { - (await this._unsub)(); - this._unsub = undefined; - } - } - - private _continue(): void { - this._step++; - } - - private async _stopNetwork(): Promise { - this._stoppingNetwork = true; - await this._getNetworkStatus(); - if (this._networkStatus?.state === ZWAVE_NETWORK_STATE_STOPPED) { - this._networkStopped(); - return; - } - - this._unsub = this.hass!.connection.subscribeEvents( - () => this._networkStopped(), - "zwave.network_stop" - ); - this.hass!.callService("zwave", "stop_network"); - } - - private async _setupZwaveJs() { - const zwaveJsConfigFlow = await startZwaveJsConfigFlow(this.hass); - showConfigFlowDialog(this, { - continueFlowId: zwaveJsConfigFlow.flow_id, - dialogClosedCallback: (params) => { - if (params.entryId) { - this._zwaveJsEntryId = params.entryId; - this._getZwaveJSNodesStatus(); - this._step = 3; - } - }, - showAdvanced: this.hass.userData?.showAdvanced, - }); - this.hass.loadBackendTranslation("title", "zwave_js", true); - } - - private async _getZwaveJSNodesStatus() { - if (this._nodeReadySubscriptions?.length) { - const unsubs = await Promise.all(this._nodeReadySubscriptions); - unsubs.forEach((unsub) => { - unsub(); - }); - } - this._nodeReadySubscriptions = []; - const networkStatus = await fetchZwaveJsNetworkStatus( - this.hass, - this._zwaveJsEntryId! - ); - const nodeStatePromisses = networkStatus.controller.nodes.map((nodeId) => - fetchZwaveNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId) - ); - const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter( - (node) => !node.ready - ); - - // eslint-disable-next-line no-console - console.log("waiting for nodes to be ready", nodesNotReady); - - this._getMigrationData(); - if (nodesNotReady.length === 0) { - this._waitingOnDevices = []; - return; - } - this._nodeReadySubscriptions = nodesNotReady.map((node) => - subscribeZwaveNodeReady( - this.hass, - this._zwaveJsEntryId!, - node.node_id, - () => { - this._getZwaveJSNodesStatus(); - } - ) - ); - const deviceReg: DeviceRegistryEntry[] = await fetchDeviceRegistry( - this.hass.connection - ); - this._waitingOnDevices = deviceReg.filter((device) => { - const identifiers = getZwaveJsIdentifiersFromDevice(device); - if ( - !identifiers || - Number(identifiers.home_id) !== networkStatus.controller.home_id - ) { - return false; - } - return nodesNotReady.some((node) => identifiers.node_id === node.node_id); - }); - } - - private async _getMigrationData() { - try { - this._migrationData = await migrateZwave( - this.hass, - this._zwaveJsEntryId!, - true - ); - } catch (err: any) { - showAlertDialog(this, { - title: "Failed to get migration data!", - text: - err.code === "unknown_command" - ? "Restart Home Assistant and try again." - : err.message, - }); - return; - } - this._migratedZwaveEntities = Object.keys( - this._migrationData.migration_entity_map - ); - if (Object.keys(this._migrationData.migration_device_map).length) { - this._fetchDevices(); - } - } - - private _fetchDevices() { - this._unsubDevices = subscribeDeviceRegistry( - this.hass.connection, - (devices) => { - if (!this._migrationData) { - return; - } - const migrationDevices = Object.keys( - this._migrationData.migration_device_map - ); - const deviceNameLookup = {}; - devices.forEach((device) => { - if (migrationDevices.includes(device.id)) { - deviceNameLookup[device.id] = computeDeviceName(device, this.hass); - } - }); - this._deviceNameLookup = deviceNameLookup; - } - ); - } - - private async _doMigrate() { - const data = await migrateZwave(this.hass, this._zwaveJsEntryId!, false); - if (!data.migrated) { - showAlertDialog(this, { title: "Migration failed!" }); - return; - } - this._step = 4; - } - - private _networkStopped(): void { - this._unsubscribe(); - this._getMigrationConfig(); - this._stoppingNetwork = false; - this._step = 2; - } - - private async _getNetworkStatus(): Promise { - this._networkStatus = await fetchNetworkStatus(this.hass!); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .content { - margin-top: 24px; - } - - .flex-container { - display: flex; - align-items: center; - } - - .flex-container ha-circular-progress { - margin-right: 20px; - } - - blockquote { - display: block; - background-color: var(--secondary-background-color); - color: var(--primary-text-color); - padding: 8px; - margin: 8px 0; - font-size: 0.9em; - font-family: monospace; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "zwave-migration": ZwaveMigration; - } -} diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts deleted file mode 100644 index f49090c2ad..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { mdiCheckboxMarkedCircle, mdiClose, mdiHelpCircle } from "@mdi/js"; -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/buttons/ha-call-api-button"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-circular-progress"; -import "../../../../../components/ha-svg-icon"; -import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-service-description"; -import { - fetchNetworkStatus, - ZWaveNetworkStatus, - ZWAVE_NETWORK_STATE_AWAKED, - ZWAVE_NETWORK_STATE_READY, - ZWAVE_NETWORK_STATE_STARTED, - ZWAVE_NETWORK_STATE_STOPPED, -} from "../../../../../data/zwave"; -import { haStyle } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import { documentationUrl } from "../../../../../util/documentation-url"; -import "../../../ha-config-section"; - -@customElement("zwave-network") -export class ZwaveNetwork extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public isWide!: boolean; - - @state() private _showHelp = false; - - @state() private _networkStatus?: ZWaveNetworkStatus; - - @state() private _unsubs: Array> = []; - - public disconnectedCallback(): void { - this._unsubscribe(); - } - - protected firstUpdated(changedProps): void { - super.firstUpdated(changedProps); - this._getNetworkStatus(); - this._subscribe(); - } - - protected render(): TemplateResult { - return html` - -
    - - ${this.hass!.localize( - "ui.panel.config.zwave.network_management.header" - )} - - -
    -
    - ${this.hass!.localize( - "ui.panel.config.zwave.network_management.introduction" - )} -

    - - ${this.hass!.localize("ui.panel.config.zwave.learn_more")} - -

    -
    - - ${this._networkStatus - ? html` - -
    - ${this._networkStatus.state === ZWAVE_NETWORK_STATE_STOPPED - ? html` - - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_stopped" - )} - ` - : this._networkStatus.state === ZWAVE_NETWORK_STATE_STARTED - ? html` - - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_starting" - )}
    - - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_starting_note" - )} - - ` - : this._networkStatus.state === ZWAVE_NETWORK_STATE_AWAKED - ? html` - - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_started" - )}
    - - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_started_note_some_queried" - )} - - ` - : this._networkStatus.state === ZWAVE_NETWORK_STATE_READY - ? html` - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_started" - )}
    - - ${this.hass!.localize( - "ui.panel.config.zwave.network_status.network_started_note_all_queried" - )} - - ` - : ""} -
    -
    - ${this._networkStatus.state >= ZWAVE_NETWORK_STATE_AWAKED - ? html` - ${this._generateServiceButton("stop_network")} - ${this._generateServiceButton("heal_network")} - ${this._generateServiceButton("test_network")} - ` - : html` ${this._generateServiceButton("start_network")} `} -
    - ${this._networkStatus.state >= ZWAVE_NETWORK_STATE_AWAKED - ? html` -
    - ${this._generateServiceButton("soft_reset")} - - ${this.hass!.localize( - "ui.panel.config.zwave.services.save_config" - )} - -
    - ` - : ""} -
    - ${this._networkStatus.state >= ZWAVE_NETWORK_STATE_AWAKED - ? html` - -
    - ${this._generateServiceButton("add_node_secure")} - ${this._generateServiceButton("add_node")} - ${this._generateServiceButton("remove_node")} -
    -
    - ${this._generateServiceButton("cancel_command")} -
    -
    - ` - : ""} - ` - : ""} -
    - `; - } - - private async _getNetworkStatus(): Promise { - this._networkStatus = await fetchNetworkStatus(this.hass!); - } - - private _subscribe(): void { - this._unsubs = [ - "zwave.network_start", - "zwave.network_stop", - "zwave.network_ready", - "zwave.network_complete", - "zwave.network_complete_some_dead", - ].map((e) => - this.hass!.connection.subscribeEvents( - (event) => this._handleEvent(event), - e - ) - ); - } - - private _unsubscribe(): void { - while (this._unsubs.length) { - this._unsubs.pop()!.then((unsub) => unsub()); - } - } - - private _handleEvent(event) { - if (event.event_type === "zwave.network_start") { - // Optimistically set the state, wait 1s and poll the backend - // The backend will still report a state of 0 when the 'network_start' - // event is first fired. - if (this._networkStatus) { - this._networkStatus = { ...this._networkStatus, state: 5 }; - } - setTimeout(() => this._getNetworkStatus, 1000); - } else { - this._getNetworkStatus(); - } - } - - private _onHelpTap(): void { - this._showHelp = !this._showHelp; - } - - private _generateServiceButton(service: string) { - return html` - - ${this.hass!.localize("ui.panel.config.zwave.services." + service)} - - - - `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .content { - margin-top: 24px; - } - - .sectionHeader { - position: relative; - padding-right: 40px; - } - - .network-status { - text-align: center; - } - - .network-status div.details { - font-size: 1.5rem; - padding: 24px; - } - - .network-status ha-svg-icon { - display: block; - margin: 0px auto 16px; - width: 48px; - height: 48px; - } - - .network-status small { - font-size: 1rem; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - - .toggle-help-icon { - position: absolute; - top: -6px; - right: 0; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - padding: 0 8px 12px; - } - - [hidden] { - display: none; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "zwave-network": ZwaveNetwork; - } -} diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-node-config.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-node-config.ts deleted file mode 100644 index 975862c679..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-node-config.ts +++ /dev/null @@ -1,388 +0,0 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import { - fetchNodeConfig, - ZWaveConfigItem, - ZWaveConfigServiceData, - ZWaveNode, -} from "../../../../../data/zwave"; -import { haStyle } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; - -@customElement("zwave-node-config") -export class ZwaveNodeConfig extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public nodes: ZWaveNode[] = []; - - @property() public config: ZWaveConfigItem[] = []; - - @property() public selectedNode = -1; - - @state() private _configItem?: ZWaveConfigItem; - - @state() private _wakeupInput = -1; - - @state() private _selectedConfigParameter = -1; - - @state() private _selectedConfigValue: number | string = -1; - - protected render(): TemplateResult { - return html` -
    - - ${"wake_up_interval" in this.nodes[this.selectedNode].attributes - ? html` -
    - -
    - ${this.hass!.localize( - "ui.panel.config.zwave.node_config.seconds" - )} -
    -
    - - ${this.hass!.localize( - "ui.panel.config.zwave.node_config.set_wakeup" - )} - -
    - ` - : ""} -
    - - - ${this.config.map( - (entityState) => html` - - ${entityState.key}: ${entityState.value.label} - - ` - )} - - -
    - ${this._configItem - ? html` - ${this._configItem.value.type === "List" - ? html` -
    - - - ${this._configItem.value.data_items.map( - (entityState) => html` - ${entityState} - ` - )} - - -
    - ` - : ""} - ${["Byte", "Short", "Int"].includes(this._configItem.value.type) - ? html` -
    - - -
    - ` - : ""} - ${["Bool", "Button"].includes(this._configItem.value.type) - ? html` -
    - - - - ${this.hass!.localize( - "ui.panel.config.zwave.node_config.true" - )} - - - ${this.hass!.localize( - "ui.panel.config.zwave.node_config.false" - )} - - - -
    - ` - : ""} -
    - ${this._configItem.value.help} -
    - ${["Bool", "Button", "Byte", "Short", "Int", "List"].includes( - this._configItem.value.type - ) - ? html` -
    - - ${this.hass!.localize( - "ui.panel.config.zwave.node_config.set_config_parameter" - )} - -
    - ` - : ""} - ` - : ""} -
    -
    - `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .content { - margin-top: 24px; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - .device-picker { - @apply --layout-horizontal; - @apply --layout-center-center; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -ms-flex-direction: row; - -webkit-flex-direction: row; - flex-direction: row; - -ms-flex-align: center; - -webkit-align-items: center; - align-items: center; - padding-left: 24px; - padding-right: 24px; - padding-bottom: 24px; - } - - .help-text { - padding-left: 24px; - padding-right: 24px; - } - - .flex { - -ms-flex: 1 1 0.000000001px; - -webkit-flex: 1; - flex: 1; - -webkit-flex-basis: 0.000000001px; - flex-basis: 0.000000001px; - } - `, - ]; - } - - protected firstUpdated(changedProps: PropertyValues): void { - super.firstUpdated(changedProps); - this.addEventListener("hass-service-called", (ev) => - this.serviceCalled(ev) - ); - } - - protected updated(changedProps: PropertyValues): void { - super.updated(changedProps); - if (changedProps.has("selectedNode")) { - this._nodesChanged(); - } - } - - private serviceCalled(ev): void { - if (ev.detail.success) { - setTimeout(() => { - this._refreshConfig(this.selectedNode); - }, 5000); - } - } - - private _nodesChanged(): void { - if (!this.nodes) { - return; - } - this._configItem = undefined; - this._wakeupInput = - this.nodes[this.selectedNode].attributes.wake_up_interval || -1; - } - - private _onWakeupIntervalChanged(value: ChangeEvent): void { - this._wakeupInput = value.detail!.value; - } - - private _computeWakeupServiceData(wakeupInput: number) { - return { - node_id: this.nodes[this.selectedNode].attributes.node_id, - value: wakeupInput, - }; - } - - private _computeSetConfigParameterServiceData(): - | ZWaveConfigServiceData - | boolean { - if (this.selectedNode === -1 || typeof this._configItem === "undefined") { - return false; - } - let valueData: number | string = ""; - if (["Short", "Byte", "Int"].includes(this._configItem!.value.type)) { - valueData = - typeof this._selectedConfigValue === "string" - ? parseInt(this._selectedConfigValue, 10) - : this._selectedConfigValue; - } - if (["Bool", "Button", "List"].includes(this._configItem!.value.type)) { - valueData = this._selectedConfigValue; - } - return { - node_id: this.nodes[this.selectedNode].attributes.node_id, - parameter: this._configItem.key, - value: valueData, - }; - } - - private _selectedConfigParameterChanged(event: ItemSelectedEvent): void { - if (event.target!.selected === -1) { - return; - } - this._selectedConfigParameter = event.target!.selected; - this._configItem = this.config[event.target!.selected]; - } - - private _configValueSelectChanged(event: ItemSelectedEvent): void { - if (event.target!.selected === -1) { - return; - } - this._selectedConfigValue = event.target!.selectedItem.textContent; - } - - private _configValueInputChanged(value: ChangeEvent): void { - this._selectedConfigValue = value.detail!.value; - } - - private async _refreshConfig(selectedNode): Promise { - const configData: ZWaveConfigItem[] = []; - const config = await fetchNodeConfig( - this.hass, - this.nodes[selectedNode].attributes.node_id - ); - - Object.keys(config).forEach((key) => { - configData.push({ - key: parseInt(key, 10), - value: config[key], - }); - }); - - this.config = configData; - this._configItem = this.config[this._selectedConfigParameter]; - } -} - -export interface ChangeEvent { - detail?: { - value?: any; - }; - target?: EventTarget; -} - -export interface PickerTarget extends EventTarget { - selected: number; - selectedItem?: any; -} - -export interface ItemSelectedEvent { - target?: PickerTarget; -} - -declare global { - interface HTMLElementTagNameMap { - "zwave-node-config": ZwaveNodeConfig; - } -} diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-node-protection.js b/src/panels/config/integrations/integration-panels/zwave/zwave-node-protection.js deleted file mode 100644 index 693ed425fb..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-node-protection.js +++ /dev/null @@ -1,179 +0,0 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../../../components/buttons/ha-call-api-button"; -import "../../../../../components/ha-card"; -import LocalizeMixin from "../../../../../mixins/localize-mixin"; -import "../../../../../styles/polymer-ha-style"; - -class ZwaveNodeProtection extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - -
    - -
    - - - - - -
    -
    - - [[localize('ui.panel.config.zwave.node_management.set_protection')]] - -
    -
    -
    -`; - } - - static get properties() { - return { - hass: Object, - - nodes: Array, - - selectedNode: { - type: Number, - value: -1, - }, - - protectionNode: { - type: Boolean, - value: false, - }, - - _protectionValueID: { - type: Number, - value: -1, - }, - - _selectedProtectionParameter: { - type: Number, - value: -1, - observer: "_computeProtectionData", - }, - - _protectionOptions: Array, - - _protection: { - type: Array, - value: () => [], - }, - - _loadedProtectionValue: { - type: String, - value: "", - }, - - _protectionData: { - type: Object, - value: {}, - }, - - _nodePath: String, - }; - } - - static get observers() { - return ["_nodesChanged(nodes, selectedNode)"]; - } - - ready() { - super.ready(); - this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev)); - } - - apiCalled(ev) { - if (ev.detail.success) { - setTimeout(() => { - this._refreshProtection(this.selectedNode); - }, 5000); - } - } - - _nodesChanged() { - if (!this.nodes) return; - if (this.protection) { - if (this.protection.length === 0) { - return; - } - let options = []; - let value_id = -1; - let selected = -1; - this.protection.forEach((item) => { - if (item.key === "options") options = item.value; - else if (item.key === "value_id") value_id = item.value; - else if (item.key === "selected") selected = item.value; - }); - this.setProperties({ - protectionNode: true, - _protectionOptions: options, - _loadedProtectionValue: selected, - _protectionValueID: value_id, - }); - } - } - - async _refreshProtection(selectedNode) { - const protectionValues = []; - const protections = await this.hass.callApi( - "GET", - `zwave/protection/${this.nodes[selectedNode].attributes.node_id}` - ); - Object.keys(protections).forEach((key) => { - protectionValues.push({ - key, - value: protections[key], - }); - }); - this.setProperties({ - _protection: protectionValues, - _selectedProtectionParameter: -1, - _loadedProtectionValue: this.protection[1].value, - }); - } - - _computeProtectionData(selectedProtectionParameter) { - if (this.selectedNode === -1 || selectedProtectionParameter === -1) return; - this._protectionData = { - selection: this._protectionOptions[selectedProtectionParameter], - value_id: this._protectionValueID, - }; - this._nodePath = `zwave/protection/${ - this.nodes[this.selectedNode].attributes.node_id - }`; - } -} - -customElements.define("zwave-node-protection", ZwaveNodeProtection); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-usercodes.js b/src/panels/config/integrations/integration-panels/zwave/zwave-usercodes.js deleted file mode 100644 index 7180f8d880..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-usercodes.js +++ /dev/null @@ -1,226 +0,0 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import "../../../../../styles/polymer-ha-style"; - -class ZwaveUsercodes extends PolymerElement { - static get template() { - return html` - -
    - -
    - - - - - -
    - - -
    -
    - `; - } - - static get properties() { - return { - hass: Object, - - nodes: Array, - - selectedNode: { - type: Number, - observer: "_selectedNodeChanged", - }, - - userCodes: Object, - - _selectedUserCode: { - type: Number, - value: -1, - observer: "_selectedUserCodeChanged", - }, - - _selectedUserCodeValue: String, - - _computedCodeOutput: { - type: String, - value: "", - }, - }; - } - - ready() { - super.ready(); - this.addEventListener("hass-service-called", (ev) => - this.serviceCalled(ev) - ); - } - - serviceCalled(ev) { - if (ev.detail.success) { - setTimeout(() => { - this._refreshUserCodes(this.selectedNode); - }, 5000); - } - } - - _isUserCodeSelected(selectedUserCode) { - if (selectedUserCode === -1) return false; - return true; - } - - _computeSelectCaptionUserCodes(stateObj) { - return `${stateObj.key}: ${stateObj.value.label}`; - } - - _selectedUserCodeChanged(selectedUserCode) { - if (this._selectedUserCode === -1 || selectedUserCode === -1) return; - const value = this.userCodes[selectedUserCode].value.code; - this.setProperties({ - _selectedUserCodeValue: this._a2hex(value), - _computedCodeOutput: `[${this._hex2a(this._a2hex(value))}]`, - }); - } - - _computeUserCodeServiceData(selectedUserCodeValue, type) { - if (this.selectedNode === -1 || !selectedUserCodeValue) return -1; - let serviceData = null; - let valueData = null; - if (type === "Add") { - valueData = this._hex2a(selectedUserCodeValue); - this._computedCodeOutput = `[${valueData}]`; - serviceData = { - node_id: this.nodes[this.selectedNode].attributes.node_id, - code_slot: this._selectedUserCode, - usercode: valueData, - }; - } - if (type === "Delete") { - serviceData = { - node_id: this.nodes[this.selectedNode].attributes.node_id, - code_slot: this._selectedUserCode, - }; - } - return serviceData; - } - - async _refreshUserCodes(selectedNode) { - this.setProperties({ _selectedUserCodeValue: "" }); - const userCodes = []; - const userCodeData = await this.hass.callApi( - "GET", - `zwave/usercodes/${this.nodes[selectedNode].attributes.node_id}` - ); - Object.keys(userCodeData).forEach((key) => { - userCodes.push({ - key, - value: userCodeData[key], - }); - }); - this.setProperties({ userCodes: userCodes }); - this._selectedUserCodeChanged(this._selectedUserCode); - } - - _a2hex(str) { - const arr = []; - let output = ""; - for (let i = 0, l = str.length; i < l; i++) { - const hex = Number(str.charCodeAt(i)).toString(16); - if (hex === "0") { - output = "00"; - } else { - output = hex; - } - arr.push("\\x" + output); - } - return arr.join(""); - } - - _hex2a(hexx) { - const hex = hexx.toString(); - const hexMod = hex.replace(/\\x/g, ""); - let str = ""; - for (let i = 0; i < hexMod.length; i += 2) { - str += String.fromCharCode(parseInt(hexMod.substr(i, 2), 16)); - } - return str; - } - - _selectedNodeChanged() { - if (this.selectedNode === -1) return; - this.setProperties({ _selecteduserCode: -1 }); - } -} - -customElements.define("zwave-usercodes", ZwaveUsercodes); diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-values.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-values.ts deleted file mode 100644 index 1e6750c6af..0000000000 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-values.ts +++ /dev/null @@ -1,109 +0,0 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import { ZWaveValue } from "../../../../../data/zwave"; -import { haStyle } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; - -@customElement("zwave-values") -export class ZwaveValues extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public values: ZWaveValue[] = []; - - @state() private _selectedValue = -1; - - protected render(): TemplateResult { - return html` -
    - -
    - - - ${this.values.map( - (item) => html` - ${this._computeCaption(item)} - ` - )} - - -
    -
    -
    - `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .content { - margin-top: 24px; - } - - ha-card { - margin: 0 auto; - max-width: 600px; - } - - .device-picker { - @apply --layout-horizontal; - @apply --layout-center-center; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -ms-flex-direction: row; - -webkit-flex-direction: row; - flex-direction: row; - -ms-flex-align: center; - -webkit-align-items: center; - align-items: center; - padding-left: 24px; - padding-right: 24px; - padding-bottom: 24px; - } - - .flex { - -ms-flex: 1 1 0.000000001px; - -webkit-flex: 1; - flex: 1; - -webkit-flex-basis: 0.000000001px; - flex-basis: 0.000000001px; - } - - .help-text { - padding-left: 24px; - padding-right: 24px; - } - `, - ]; - } - - private _computeCaption(item) { - let out = `${item.value.label}`; - out += ` (${this.hass.localize("ui.panel.config.zwave.common.instance")}:`; - out += ` ${item.value.instance},`; - out += ` ${this.hass.localize("ui.panel.config.zwave.common.index")}:`; - out += ` ${item.value.index})`; - return out; - } -} - -declare global { - interface HTMLElementTagNameMap { - "zwave-values": ZwaveValues; - } -} diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts index 3c75b59df0..9b5a5c8e37 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts @@ -384,7 +384,9 @@ class ZWaveJSConfigDashboard extends LitElement { if (!this.configEntryId) { return; } - const configEntries = await getConfigEntries(this.hass); + const configEntries = await getConfigEntries(this.hass, { + domain: "zwave_js", + }); this._configEntry = configEntries.find( (entry) => entry.entry_id === this.configEntryId! ); @@ -467,7 +469,9 @@ class ZWaveJSConfigDashboard extends LitElement { if (!this.configEntryId) { return; } - const configEntries = await getConfigEntries(this.hass); + const configEntries = await getConfigEntries(this.hass, { + domain: "zwave_js", + }); const configEntry = configEntries.find( (entry) => entry.entry_id === this.configEntryId ); diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts index 4b48ebb116..b9b2359dcf 100644 --- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts +++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts @@ -1,4 +1,5 @@ import "@material/mwc-button/mwc-button"; +import { mdiSlopeUphill } from "@mdi/js"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -22,6 +23,7 @@ import { import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum"; import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed"; import { showFixStatisticsUnsupportedUnitMetadataDialog } from "./show-dialog-statistics-fix-unsupported-unit-meta"; @@ -111,6 +113,24 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { : ""}`, width: "113px", }, + actions: { + title: "", + label: localize("ui.panel.developer-tools.tabs.statistics.adjust_sum"), + type: "icon-button", + template: (_info, statistic: StatisticsMetaData) => + statistic.has_sum + ? html` + + ` + : "", + }, }) ); @@ -127,6 +147,13 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { `; } + private _showStatisticsAdjustSumDialog(ev) { + ev.stopPropagation(); + showStatisticsAdjustSumDialog(this, { + statistic: ev.currentTarget.statistic, + }); + } + private _rowClicked(ev) { const id = ev.detail.id; if (id in this.hass.states) { @@ -185,6 +212,8 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { source: "", state: this.hass.states[statisticId], issues: issues[statisticId], + has_mean: false, + has_sum: false, }); } }); diff --git a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts new file mode 100644 index 0000000000..bae0576f9e --- /dev/null +++ b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts @@ -0,0 +1,374 @@ +import "@material/mwc-button/mwc-button"; +import "@material/mwc-list/mwc-list-item"; +import { mdiChevronRight } from "@mdi/js"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { formatDateTime } from "../../../common/datetime/format_date_time"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-circular-progress"; +import "../../../components/ha-dialog"; +import "../../../components/ha-form/ha-form"; +import "../../../components/ha-selector/ha-selector-datetime"; +import "../../../components/ha-selector/ha-selector-number"; +import "../../../components/ha-svg-icon"; +import { + adjustStatisticsSum, + fetchStatistics, + StatisticValue, +} from "../../../data/history"; +import type { DateTimeSelector, NumberSelector } from "../../../data/selector"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; +import { haStyle, haStyleDialog } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { showToast } from "../../../util/toast"; +import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum"; + +/* eslint-disable lit/no-template-arrow */ + +@customElement("dialog-statistics-adjust-sum") +export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: DialogStatisticsAdjustSumParams; + + @state() private _busy = false; + + @state() private _moment?: string; + + @state() private _stats5min?: StatisticValue[]; + + @state() private _statsHour?: StatisticValue[]; + + @state() private _chosenStat?: StatisticValue; + + private _origAmount?: number; + + @state() private _amount?: number; + + private _dateTimeSelector: DateTimeSelector = { + datetime: {}, + }; + + private _amountSelector = memoizeOne( + (unit_of_measurement: string): NumberSelector => ({ + number: { + step: 0.01, + unit_of_measurement, + mode: "box", + }, + }) + ); + + public showDialog(params: DialogStatisticsAdjustSumParams): void { + this._params = params; + const now = new Date(); + this._moment = `${now.getFullYear()}-${ + now.getMonth() + 1 + }-${now.getDate()} ${now.getHours()}:${ + now.getMinutes() - (now.getMinutes() % 5) + }:00`; + this._fetchStats(); + } + + public closeDialog(): void { + this._params = undefined; + this._moment = undefined; + this._stats5min = undefined; + this._statsHour = undefined; + this._origAmount = undefined; + this._amount = undefined; + this._chosenStat = undefined; + this._busy = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult | void { + if (!this._params) { + return html``; + } + + let content: TemplateResult; + + if (!this._chosenStat) { + content = this._renderPickStatistic(); + } else { + content = this._renderAdjustStat(); + } + + return html` + + ${content} + + `; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + if (changedProps.size !== 1 || !changedProps.has("hass")) { + return true; + } + // We only respond to hass changes if the translations changed + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + return !oldHass || oldHass.localize !== this.hass.localize; + } + + private _renderPickStatistic() { + let stats: TemplateResult; + + if (!this._stats5min || !this._statsHour) { + stats = html``; + } else if (this._statsHour.length < 2 && this._stats5min.length < 2) { + stats = html`

    No statistics found for this period.

    `; + } else { + const data = + this._stats5min.length >= 2 ? this._stats5min : this._statsHour; + const unit = this._params!.statistic.unit_of_measurement; + const rows: TemplateResult[] = []; + for (let i = 1; i < data.length; i++) { + const stat = data[i]; + const growth = Math.round((stat.sum! - data[i - 1].sum!) * 100) / 100; + rows.push(html` + { + this._chosenStat = stat; + this._origAmount = growth; + this._amount = growth; + }} + > + ${growth} ${unit} + + ${formatDateTime(new Date(stat.start), this.hass.locale)} + + + + `); + } + stats = html`${rows}`; + } + + return html` +
    + Sometimes the statistics end up being incorrect for a specific point in + time. This can mess up your beautiful graphs! Select a time below to + find the bad moment and adjust the data. +
    + +
    ${stats}
    + + `; + } + + private _dateTimeSelectorChanged(ev) { + this._moment = ev.detail.value; + this._fetchStats(); + } + + private _renderAdjustStat() { + return html` +
    + ${this._params!.statistic.name || this._params!.statistic.statistic_id} +
    + +
    + Start + ${formatDateTime( + new Date(this._chosenStat!.start), + this.hass.locale + )} +
    + +
    + End + ${formatDateTime( + new Date(this._chosenStat!.end), + this.hass.locale + )} +
    + + { + this._amount = ev.detail.value; + }} + > + + { + this._fixIssue(); + }} + > + { + this._chosenStat = undefined; + }} + > + `; + } + + private async _fetchStats(): Promise { + this._stats5min = undefined; + this._statsHour = undefined; + const statId = this._params!.statistic.statistic_id; + const moment = new Date(this._moment!); + + // Search 3 hours before and 3 hours after chosen time + const hourStatStart = new Date(moment.getTime()); + hourStatStart.setTime(hourStatStart.getTime() - 3 * 3600 * 1000); + const hourStatEnd = new Date(moment.getTime()); + hourStatEnd.setTime(hourStatEnd.getTime() + 3 * 3600 * 1000); + + const statsHourData = await fetchStatistics( + this.hass, + hourStatStart, + hourStatEnd, + [statId], + "hour" + ); + this._statsHour = + statId in statsHourData ? statsHourData[statId].slice(0, 6) : []; + + // Can't have 5 min data if no hourly data + if (this._statsHour.length === 0) { + this._stats5min = []; + return; + } + + // Search 15 minutes before and 15 minutes after chosen time + const minStatStart = new Date(moment.getTime()); + minStatStart.setTime(minStatStart.getTime() - 15 * 60 * 1000); + const minStatEnd = new Date(moment.getTime()); + minStatEnd.setTime(minStatEnd.getTime() + 15 * 60 * 1000); + + const stats5MinData = await fetchStatistics( + this.hass, + minStatStart, + minStatEnd, + [statId], + "5minute" + ); + + this._stats5min = + statId in stats5MinData ? stats5MinData[statId].slice(0, 6) : []; + } + + private async _fixIssue(): Promise { + this._busy = true; + try { + await adjustStatisticsSum( + this.hass, + this._params!.statistic.statistic_id, + this._chosenStat!.start, + this._amount! - this._origAmount! + ); + } catch (err: any) { + this._busy = false; + showAlertDialog(this, { + text: `Error adjusting sum: ${err.message || err}`, + }); + return; + } + showToast(this, { + message: "Statistic sum adjusted", + }); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + @media all and (max-width: 450px), all and (max-height: 500px) { + /* overrule the ha-style-dialog max-height on small screens */ + ha-dialog { + --mdc-dialog-max-height: 100%; + height: 100%; + } + } + + @media all and (min-width: 850px) { + ha-dialog { + --mdc-dialog-max-height: 80%; + --mdc-dialog-max-height: 80%; + } + } + + @media all and (min-width: 451px) and (min-height: 501px) { + ha-dialog { + --mdc-dialog-max-width: 480px; + } + } + + .text-content, + ha-selector-datetime, + ha-selector-number { + margin-bottom: 20px; + } + mwc-list-item { + margin: 0 -24px; + --mdc-list-side-padding: 24px; + } + .table-row { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + } + .stat-list { + min-height: 360px; + display: flex; + flex-direction: column; + } + .stat-list ha-circular-progress { + margin: 0 auto; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-statistics-adjust-sum": DialogStatisticsFixUnsupportedUnitMetadata; + } +} diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts index 1ce0c607ce..3168bffc9f 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts @@ -11,7 +11,7 @@ import { } from "../../../data/history"; import "../../../components/ha-formfield"; import "../../../components/ha-radio"; -import { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed"; +import type { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed"; @customElement("dialog-statistics-fix-units-changed") export class DialogStatisticsFixUnitsChanged extends LitElement { diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts b/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts index 93d1e320b1..4bfaebe489 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts @@ -8,7 +8,7 @@ import { HomeAssistant } from "../../../types"; import { updateStatisticsMetadata } from "../../../data/history"; import "../../../components/ha-formfield"; import "../../../components/ha-radio"; -import { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta"; +import type { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta"; @customElement("dialog-statistics-fix-unsupported-unit-meta") export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts new file mode 100644 index 0000000000..1db2c76307 --- /dev/null +++ b/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts @@ -0,0 +1,20 @@ +import { fireEvent } from "../../../common/dom/fire_event"; +import { StatisticsMetaData } from "../../../data/history"; + +export const loadAdjustSumDialog = () => + import("./dialog-statistics-adjust-sum"); + +export interface DialogStatisticsAdjustSumParams { + statistic: StatisticsMetaData; +} + +export const showStatisticsAdjustSumDialog = ( + element: HTMLElement, + detailParams: DialogStatisticsAdjustSumParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-statistics-adjust-sum", + dialogImport: loadAdjustSumDialog, + dialogParams: detailParams, + }); +}; diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index 50e26074f2..f1d2793ae0 100644 --- a/src/panels/lovelace/cards/hui-media-control-card.ts +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -28,6 +28,7 @@ import { computeMediaControls, computeMediaDescription, getCurrentProgress, + handleMediaControlClick, MediaPickedEvent, MediaPlayerEntity, SUPPORT_BROWSE_MEDIA, @@ -174,7 +175,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { UNAVAILABLE_STATES.includes(entityState) || (entityState === "off" && !supportsFeature(stateObj, SUPPORT_TURN_ON)); const hasNoImage = !this._image; - const controls = computeMediaControls(stateObj); + const controls = computeMediaControls(stateObj, false); const showControls = controls && (!this._veryNarrow || @@ -504,10 +505,11 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { } private _handleClick(e: MouseEvent): void { - const action = (e.currentTarget! as HTMLElement).getAttribute("action")!; - this.hass!.callService("media_player", action, { - entity_id: this._config!.entity, - }); + handleMediaControlClick( + this.hass!, + this._stateObj!, + (e.currentTarget as HTMLElement).getAttribute("action")! + ); } private _updateProgressBar(): void { diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 0d3fd0cb9a..8ffdd43709 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -238,7 +238,10 @@ const computeDefaultViewStates = ( const hiddenEntities = new Set( entityEntries .filter( - (entry) => entry.entity_category || HIDE_PLATFORM.has(entry.platform) + (entry) => + entry.entity_category || + HIDE_PLATFORM.has(entry.platform) || + entry.hidden_by ) .map((entry) => entry.entity_id) ); diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index aef797e278..7e34b16c70 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -116,7 +116,7 @@ export class HuiCardOptions extends LitElement { outline: 2px solid var(--primary-color); } - :host:not(.panel) ::slotted(*) { + :host(:not(.panel)) ::slotted(*) { display: block; } diff --git a/src/panels/lovelace/editor/config-elements/config-elements-style.ts b/src/panels/lovelace/editor/config-elements/config-elements-style.ts index 061d3aff36..2f068a1f91 100644 --- a/src/panels/lovelace/editor/config-elements/config-elements-style.ts +++ b/src/panels/lovelace/editor/config-elements/config-elements-style.ts @@ -1,6 +1,10 @@ import { css } from "lit"; export const configElementStyle = css` + .card-config { + /* Cancels overlapping Margins for HAForm + Card Config options */ + overflow: auto; + } ha-switch { padding: 16px 6px; } @@ -19,11 +23,11 @@ export const configElementStyle = css` .suffix { margin: 0 8px; } - hui-theme-select-editor, hui-action-editor, ha-select, ha-textfield, ha-icon-picker { margin-top: 8px; + display: block; } `; diff --git a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts index ae8f79bd27..96f98fbbc2 100644 --- a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts @@ -99,6 +99,14 @@ export class HuiAlarmPanelCardEditor return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.name`); } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.alarm-panel.${ schema.name === "states" ? "available_states" : schema.name diff --git a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts index 447c1ccd2e..ed4114bda5 100644 --- a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts @@ -69,6 +69,12 @@ export class HuiAreaCardEditor private _computeLabelCallback = (schema: HaFormSchema) => { switch (schema.name) { + case "theme": + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; case "area": return this.hass!.localize("ui.panel.lovelace.editor.card.area.name"); case "navigation_path": diff --git a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts index e5f8525d99..90bc7e51d3 100644 --- a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts @@ -1,22 +1,22 @@ +import type { HassEntity } from "home-assistant-js-websocket"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { assert, boolean, object, optional, string, assign } from "superstruct"; -import type { HassEntity } from "home-assistant-js-websocket"; import memoizeOne from "memoize-one"; +import { assert, assign, boolean, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeDomain } from "../../../../common/entity/compute_domain"; +import { domainIcon } from "../../../../common/entity/domain_icon"; +import "../../../../components/ha-form/ha-form"; +import type { HaFormSchema } from "../../../../components/ha-form/types"; import { ActionConfig } from "../../../../data/lovelace"; import type { HomeAssistant } from "../../../../types"; import type { ButtonCardConfig } from "../../cards/types"; import "../../components/hui-action-editor"; -import "../../../../components/ha-form/ha-form"; import type { LovelaceCardEditor } from "../../types"; import { actionConfigStruct } from "../structs/action-struct"; +import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import type { EditorTarget } from "../types"; import { configElementStyle } from "./config-elements-style"; -import { baseLovelaceCardConfig } from "../structs/base-card-struct"; -import { computeDomain } from "../../../../common/entity/compute_domain"; -import { domainIcon } from "../../../../common/entity/domain_icon"; -import type { HaFormSchema } from "../../../../components/ha-form/types"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -149,38 +149,36 @@ export class HuiButtonCardEditor @value-changed=${this._valueChanged} >
    -
    - - -
    + +
    `; } @@ -202,6 +200,14 @@ export class HuiButtonCardEditor )}`; } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-calendar-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-calendar-card-editor.ts index 30c81af52c..b1e6baa3e5 100644 --- a/src/panels/lovelace/editor/config-elements/hui-calendar-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-calendar-card-editor.ts @@ -121,6 +121,14 @@ export class HuiCalendarCardEditor return this.hass!.localize("ui.panel.lovelace.editor.card.generic.title"); } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.calendar.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts index b0c418718e..536eede67c 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts @@ -31,7 +31,7 @@ import "../../../../components/ha-icon"; import "../../../../components/ha-switch"; import type { HomeAssistant } from "../../../../types"; import type { EntitiesCardConfig } from "../../cards/types"; -import "../../components/hui-theme-select-editor"; +import "../../../../components/ha-theme-picker"; import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types"; import type { LovelaceRowConfig } from "../../entity-rows/types"; import { headerFooterConfigStructs } from "../../header-footer/structs"; @@ -265,12 +265,17 @@ export class HuiEntitiesCardEditor .configValue=${"title"} @input=${this._valueChanged} > - + >
    - this.hass!.localize( - `ui.panel.lovelace.editor.card.glance.${schema.name}` - ) || - this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); + private _computeLabelCallback = (schema: HaFormSchema) => { + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return ( + this.hass!.localize( + `ui.panel.lovelace.editor.card.glance.${schema.name}` + ) || + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) + ); + }; } declare global { diff --git a/src/panels/lovelace/editor/config-elements/hui-graph-footer-editor.ts b/src/panels/lovelace/editor/config-elements/hui-graph-footer-editor.ts index 736b9ed5a8..ef8c6f7989 100644 --- a/src/panels/lovelace/editor/config-elements/hui-graph-footer-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-graph-footer-editor.ts @@ -4,8 +4,8 @@ import { assert } from "superstruct"; import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event"; import "../../../../components/entity/ha-entity-picker"; import "../../../../components/ha-formfield"; -import "../../../../components/ha-textfield"; import "../../../../components/ha-switch"; +import "../../../../components/ha-textfield"; import type { HomeAssistant } from "../../../../types"; import { graphHeaderFooterConfigStruct } from "../../header-footer/structs"; import { GraphHeaderFooterConfig } from "../../header-footer/types"; @@ -50,15 +50,14 @@ export class HuiGraphFooterEditor
    diff --git a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts index 7f472d39a1..b5e83f7c81 100644 --- a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts @@ -75,6 +75,14 @@ export class HuiHumidifierCardEditor ); } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts index 8995c7a6ff..42e47ef81a 100644 --- a/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts @@ -179,6 +179,14 @@ export class HuiLightCardEditor ); } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-logbook-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-logbook-card-editor.ts index 427535e412..f789c09bbe 100644 --- a/src/panels/lovelace/editor/config-elements/hui-logbook-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-logbook-card-editor.ts @@ -96,11 +96,24 @@ export class HuiLogbookCardEditor fireEvent(this, "config-changed", { config: ev.detail.value }); } - private _computeLabelCallback = (schema: HaFormSchema) => - this.hass!.localize( - `ui.panel.lovelace.editor.card.generic.${schema.name}` - ) || - this.hass!.localize(`ui.panel.lovelace.editor.card.logbook.${schema.name}`); + private _computeLabelCallback = (schema: HaFormSchema) => { + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + + return ( + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || + this.hass!.localize( + `ui.panel.lovelace.editor.card.logbook.${schema.name}` + ) + ); + }; } declare global { diff --git a/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts index fe1b3360c4..54dc74bcd7 100644 --- a/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts @@ -58,13 +58,23 @@ export class HuiMarkdownCardEditor fireEvent(this, "config-changed", { config: ev.detail.value }); } - private _computeLabelCallback = (schema: HaFormSchema) => - this.hass!.localize( - `ui.panel.lovelace.editor.card.generic.${schema.name}` - ) || - this.hass!.localize( - `ui.panel.lovelace.editor.card.markdown.${schema.name}` + private _computeLabelCallback = (schema: HaFormSchema) => { + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return ( + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || + this.hass!.localize( + `ui.panel.lovelace.editor.card.markdown.${schema.name}` + ) ); + }; } declare global { diff --git a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts index 012dac5a35..9470be0a78 100644 --- a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts @@ -3,9 +3,9 @@ import { customElement, property, state } from "lit/decorators"; import { assert, assign, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/entity/ha-entity-picker"; +import "../../../../components/ha-theme-picker"; import { HomeAssistant } from "../../../../types"; import { MediaControlCardConfig } from "../../cards/types"; -import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { EditorTarget, EntitiesEditorEvent } from "../types"; @@ -50,24 +50,28 @@ export class HuiMediaControlCardEditor return html`
    - + >
    `; } diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts index 0df731ba97..8174a049e2 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts @@ -1,17 +1,17 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { assert, object, optional, string, assign } from "superstruct"; +import { assert, assign, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import { ActionConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { PictureCardConfig } from "../../cards/types"; import "../../components/hui-action-editor"; -import "../../components/hui-theme-select-editor"; +import "../../../../components/ha-theme-picker"; import { LovelaceCardEditor } from "../../types"; import { actionConfigStruct } from "../structs/action-struct"; +import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { EditorTarget } from "../types"; import { configElementStyle } from "./config-elements-style"; -import { baseLovelaceCardConfig } from "../structs/base-card-struct"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -72,38 +72,41 @@ export class HuiPictureCardEditor .configValue=${"image"} @input=${this._valueChanged} > - -
    - - -
    + > + +
    `; } diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts index 0d315cc889..7367ac09ef 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts @@ -108,32 +108,30 @@ export class HuiPictureEntityCardEditor @value-changed=${this._valueChanged} >
    -
    - - -
    + +
    `; } @@ -172,6 +170,14 @@ export class HuiPictureEntityCardEditor ); } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return ( this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts index adcd07a57e..18e46b154e 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts @@ -1,13 +1,13 @@ -import "../../components/hui-action-editor"; -import "../../../../components/ha-form/ha-form"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { array, assert, assign, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-form/ha-form"; import type { HaFormSchema } from "../../../../components/ha-form/types"; import type { ActionConfig } from "../../../../data/lovelace"; import type { HomeAssistant } from "../../../../types"; import type { PictureGlanceCardConfig } from "../../cards/types"; +import "../../components/hui-action-editor"; import "../../components/hui-entity-editor"; import type { EntityConfig } from "../../entity-rows/types"; import type { LovelaceCardEditor } from "../../types"; @@ -96,28 +96,26 @@ export class HuiPictureGlanceCardEditor @value-changed=${this._valueChanged} >
    -
    - - -
    + + - + >
    `; } diff --git a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts index ed4ab2a61b..78d6dc3a97 100644 --- a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts @@ -71,6 +71,14 @@ export class HuiThermostatCardEditor ); } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts index cd6c2ab227..70a310b5da 100644 --- a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts @@ -184,6 +184,14 @@ export class HuiWeatherForecastCardEditor )})`; } + if (schema.name === "theme") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + } + return ( this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` diff --git a/src/panels/lovelace/hui-editor.ts b/src/panels/lovelace/hui-editor.ts index 17f35b3d10..ce6c15e99e 100644 --- a/src/panels/lovelace/hui-editor.ts +++ b/src/panels/lovelace/hui-editor.ts @@ -148,8 +148,8 @@ class LovelaceFullConfigEditor extends LitElement { } app-toolbar { - background-color: var(--dark-background-color, #455a64); - color: var(--dark-text-color); + background-color: var(--app-header-edit-background-color, #455a64); + color: var(--app-header-edit-text-color, #fff); } mwc-button[disabled] { diff --git a/src/panels/media-browser/browser-media-player.ts b/src/panels/media-browser/browser-media-player.ts index b4096f9a7f..c4c4bddc22 100644 --- a/src/panels/media-browser/browser-media-player.ts +++ b/src/panels/media-browser/browser-media-player.ts @@ -84,7 +84,7 @@ export class BrowserMediaPlayer { last_changed: now, last_updated: now, attributes: {}, - context: { id: "", user_id: null }, + context: { id: "", user_id: null, parent_id: null }, }; } diff --git a/src/panels/media-browser/ha-bar-media-player.ts b/src/panels/media-browser/ha-bar-media-player.ts index 3e913c6e43..bfc2e63656 100644 --- a/src/panels/media-browser/ha-bar-media-player.ts +++ b/src/panels/media-browser/ha-bar-media-player.ts @@ -39,6 +39,7 @@ import { computeMediaDescription, formatMediaTime, getCurrentProgress, + handleMediaControlClick, MediaPlayerEntity, MediaPlayerItem, setMediaPlayerVolume, @@ -173,7 +174,7 @@ export class BarMediaPlayer extends LitElement { } const controls = !this.narrow - ? computeMediaControls(stateObj) + ? computeMediaControls(stateObj, true) : (stateObj.state === "playing" && (supportsFeature(stateObj, SUPPORT_PAUSE) || supportsFeature(stateObj, SUPPORT_STOP))) || @@ -490,9 +491,11 @@ export class BarMediaPlayer extends LitElement { const action = (e.currentTarget! as HTMLElement).getAttribute("action")!; if (!this._browserPlayer) { - this.hass!.callService("media_player", action, { - entity_id: this.entityId, - }); + handleMediaControlClick( + this.hass!, + this._stateObj!, + (e.currentTarget as HTMLElement).getAttribute("action")! + ); return; } if (action === "media_pause") { diff --git a/src/panels/profile/ha-pick-theme-row.ts b/src/panels/profile/ha-pick-theme-row.ts index b0147b6842..59d9774712 100644 --- a/src/panels/profile/ha-pick-theme-row.ts +++ b/src/panels/profile/ha-pick-theme-row.ts @@ -173,6 +173,9 @@ export class HaPickThemeRow extends LitElement { } private _supportsModeSelection(themeName: string): boolean { + if (!(themeName in this.hass.themes.themes)) { + return false; // User's theme no longer exists + } return "modes" in this.hass.themes.themes[themeName]; } diff --git a/src/resources/markdown_worker.ts b/src/resources/markdown_worker.ts index 38a069c1d7..a69bd9c02e 100644 --- a/src/resources/markdown_worker.ts +++ b/src/resources/markdown_worker.ts @@ -47,6 +47,7 @@ const renderMarkdown = ( input: ["type", "disabled", "checked"], "ha-icon": ["icon"], "ha-svg-icon": ["path"], + "ha-alert": ["alert-type", "title"], }; } diff --git a/src/translations/en.json b/src/translations/en.json index e25991e80f..86bc60cbfc 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -102,6 +102,11 @@ "integration": "Integration", "config_entry": "Config entry", "device": "Device" + }, + "hidden_by": { + "user": "User", + "integration": "Integration", + "device": "Device" } }, "ui": { @@ -206,6 +211,8 @@ "media_volume_down": "Volume down", "media_volume_mute": "Volume mute", "media_volume_unmute": "Volume unmute", + "repeat_set": "Repeat mode", + "shuffle_set": "Shuffle", "text_to_speak": "Text to speak", "nothing_playing": "Nothing Playing" }, @@ -223,6 +230,11 @@ "service": { "run": "Run" }, + "update": { + "installing": "Installing", + "installing_with_progress": "Installing ({progress}%)", + "up_to_date": "Up-to-date" + }, "timer": { "actions": { "start": "start", @@ -292,6 +304,7 @@ "remove": "Remove", "enable": "Enable", "disable": "Disable", + "hide": "Hide", "close": "Close", "clear": "Clear", "leave": "Leave", @@ -390,6 +403,10 @@ "add_device_id": "Choose device", "add_entity_id": "Choose entity" }, + "theme-picker": { + "theme": "Theme", + "no_theme": "No theme" + }, "user-picker": { "no_user": "No user", "add_user": "Add user", @@ -713,6 +730,14 @@ "rising": "Rising", "setting": "Setting" }, + "update": { + "current_version": "Current version", + "latest_version": "Latest version", + "release_announcement": "Read release announcement", + "skip": "Skip", + "install": "Install", + "create_backup": "Create backup before updating" + }, "updater": { "title": "Update Instructions" }, @@ -761,29 +786,67 @@ "icon": "Icon", "icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'", "entity_id": "Entity ID", + "unit_of_measurement": "Unit of Measurement", "device_class": "Show as", "device_classes": { "binary_sensor": { "door": "Door", "garage_door": "Garage door", "window": "Window", - "opening": "Other" + "opening": "Opening", + "battery": "Battery", + "battery_charging": "Battery charging", + "carbon_monoxide": "Carbon monoxide", + "cold": "Cold", + "connectivity": "Connectivity", + "gas": "Gas", + "heat": "Heat", + "light": "Light", + "lock": "Lock", + "moisture": "Moisture", + "motion": "Motion", + "moving": "Moving", + "occupancy": "Occupancy", + "plug": "Plug", + "power": "Power", + "presence": "Presence", + "problem": "Problem", + "running": "Running", + "safety": "Safety", + "smoke": "Smoke", + "sound": "Sound", + "tamper": "Tamper", + "update": "Update", + "vibration": "Vibration" }, "cover": { "door": "Door", "garage": "Garage door", "gate": "Gate", - "window": "Window" + "window": "Window", + "shade": "Shade", + "awning": "Awning", + "blind": "Blind", + "curtain": "Curtain", + "damper": "Damper", + "shutter": "Shutter" } }, "unavailable": "This entity is unavailable.", - "enabled_label": "Enable entity", - "enabled_cause": "Disabled by {cause}.", + "entity_status": "Entity status", + "change_area": "Change Area", + "enabled_label": "Enabled", + "disabled_label": "Disabled", + "enabled_cause": "Cannot change status. Disabled by {cause}.", + "hidden_label": "Hidden", + "hidden_cause": "Hidden by {cause}.", "device_disabled": "The device of this entity is disabled.", "open_device_settings": "Open device settings", + "switch_as_x_confirm": "This switch will be hidden and a new {domain} will be added. Your existing configurations using the switch will continue to work.", "enabled_description": "Disabled entities will not be added to Home Assistant.", "enabled_delay_confirm": "The enabled entities will be added to Home Assistant in {delay} seconds", "enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities", + "hidden_description": "Hidden entities will not be shown on your dashboard. Their history is still tracked and you can still interact with them with services.", "delete": "Delete", "confirm_delete": "Are you sure you want to delete this entity?", "update": "Update", @@ -792,7 +855,8 @@ "area": "Set entity area only", "area_note": "By default the entities of a device are in the same area as the device. If you change the area of this entity, it will no longer follow the area of the device.", "follow_device_area": "Follow device area", - "change_device_area": "Change device area" + "change_device_area": "Change device area", + "configure_state": "Configure State" } }, "helper_settings": { @@ -840,7 +904,8 @@ "step": "Step size" }, "timer": { - "duration": "Duration" + "duration": "Duration", + "restore": "Restore?" } }, "options_flow": { @@ -1007,6 +1072,10 @@ "title": "Automations & Scenes", "description": "Manage automations, scenes, scripts and helpers" }, + "backup": { + "title": "Backup", + "description": "Generate backups of your Home Assistant configuration" + }, "blueprints": { "title": "Blueprints", "description": "Pre-made automations and scripts by the community" @@ -1047,9 +1116,9 @@ "learn_more": "Learn more" }, "updates": { - "check_unavailable": { + "no_update_entities": { "title": "Unable to check for updates", - "description": "You need to run the Home Assistant operating system to be able to check and install updates from the Home Assistant user interface." + "description": "You do not have any integrations that provide updates." }, "check_updates": "Check for updates", "no_new_updates": "No new updates found", @@ -1097,6 +1166,27 @@ "confirmation_text": "All devices in this area will become unassigned." } }, + "backup": { + "caption": "[%key:ui::panel::config::dashboard::backup::title%]", + "create_backup": "[%key:supervisor::backup::create_backup%]", + "creating_backup": "Backup is currently being created", + "download_backup": "[%key:supervisor::backup::download_backup%]", + "remove_backup": "[%key:supervisor::backup::delete_backup_title%]", + "name": "[%key:supervisor::backup::name%]", + "size": "[%key:supervisor::backup::size%]", + "created": "[%key:supervisor::backup::created%]", + "no_backups": "[%key:supervisor::backup::no_backups%]", + "create": { + "title": "Create backup", + "description": "Create a backup of your current configuration directory, this will take some time.", + "confirm": "create" + }, + "remove": { + "title": "Remove backup", + "description": "Are you sure you want to remove the backup with the name {name}?", + "confirm": "[%key:ui::common::remove%]" + } + }, "tag": { "caption": "Tags", "description": "Trigger automations when an NFC tag, QR code, etc. is scanned", @@ -1349,13 +1439,24 @@ "metric_example": "Celsius, kilograms", "find_currency_value": "Find your value", "save_button": "Save", - "external_url": "External URL", - "internal_url": "Internal URL", "currency": "Currency" } } } }, + "url": { + "caption": "Home Assistant URL", + "description": "Configure what website addresses Home Assistant should share with other devices when they need to fetch data from Home Assistant (eg. to play text-to-speech or other hosted media).", + "internal_url_label": "Local Network", + "external_url_label": "Internet", + "external_use_ha_cloud": "Use Home Assistant Cloud", + "external_get_ha_cloud": "Access from anywhere using Home Assistant Cloud", + "ha_cloud_remote_not_enabled": "Your Home Assistant Cloud remote connection is currently not enabled.", + "enable_remote": "[%key:ui::common::enable%]", + "internal_url_automatic": "Automatic", + "internal_url_https_error_title": "Invalid local network URL", + "internal_url_https_error_description": "You have configured an HTTPS certificate in Home Assistant. This means that your internal URL needs to be set to a domain covered by the certficate." + }, "info": { "caption": "Info", "copy_menu": "Copy menu", @@ -1408,7 +1509,7 @@ "error_from_custom_integration": "This error originated from a custom integration." }, "lovelace": { - "caption": "Lovelace Dashboards", + "caption": "Dashboards", "description": "Create customized sets of cards to control your home", "dashboards": { "default_dashboard": "This is the default dashboard", @@ -1434,7 +1535,7 @@ "confirm_delete_title": "Delete {dashboard_title}?", "confirm_delete_text": "Your dashboard will be permanently deleted.", "cant_edit_yaml": "Dashboards created in YAML cannot be edited from the UI. Change them in configuration.yaml.", - "cant_edit_default": "The default Lovelace dashboard, Overview, cannot be edited from the UI. You can hide it by setting another dashboard as default.", + "cant_edit_default": "The default dashboard, Overview, cannot be edited from the UI. You can hide it by setting another dashboard as default.", "detail": { "edit_dashboard": "Edit dashboard", "new_dashboard": "Add new dashboard", @@ -1472,7 +1573,7 @@ "confirm_delete": "Are you sure you want to delete this resource?", "refresh_header": "Do you want to refresh?", "refresh_body": "You have to refresh the page to complete the removal. Do you want to refresh now?", - "cant_edit_yaml": "You are using Lovelace in YAML mode, therefore you cannot manage your resources through the UI. Manage them in configuration.yaml.", + "cant_edit_yaml": "You are using your dashboard in YAML mode, therefore you cannot manage your resources through the UI. Manage them in configuration.yaml.", "detail": { "new_resource": "Add new resource", "dismiss": "Close", @@ -1740,6 +1841,9 @@ "introduction": "Conditions are optional and will prevent the automation from running unless all conditions are satisfied.", "learn_more": "Learn more about conditions", "add": "Add condition", + "test": "Test", + "invalid_condition": "Invalid condition configuration", + "test_failed": "Error occurred while testing condition", "duplicate": "[%key:ui::panel::config::automation::editor::triggers::duplicate%]", "delete": "[%key:ui::panel::mailbox::delete_button%]", "delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]", @@ -2356,10 +2460,10 @@ "sensor": "Sensors", "diagnostic": "Diagnostic", "config": "Configuration", - "add_entities_lovelace": "Add to Lovelace", + "add_entities_lovelace": "Add to dashboard", "none": "This device has no entities", - "hide_disabled": "Hide disabled", - "disabled_entities": "+{count} {count, plural,\n one {disabled entity}\n other {disabled entities}\n}" + "show_less": "Show less", + "hidden_entities": "+{count} {count, plural,\n one {entity}\n other {entities}\n} not shown" }, "confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?", "confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!", @@ -2384,7 +2488,7 @@ "filter": { "filter": "Filter", "show_disabled": "Show disabled devices", - "hidden_devices": "{number} hidden {number, plural,\n one {device}\n other {devices}\n}", + "hidden_devices": "{number} {number, plural,\n one {device}\n other {devices}\n} not shown", "show_all": "Show all" } } @@ -2399,10 +2503,11 @@ "search": "Search entities", "filter": { "filter": "Filter", + "show_hidden": "Show hidden entities", "show_disabled": "Show disabled entities", "show_unavailable": "Show unavailable entities", "show_readonly": "Show read-only entities", - "hidden_entities": "{number} hidden {number, plural,\n one {entity}\n other {entities}\n}", + "hidden_entities": "{number} {number, plural,\n one {entity}\n other {entities}\n} not shown", "show_all": "Show all" }, "status": { @@ -2410,6 +2515,7 @@ "unavailable": "Unavailable", "disabled": "Disabled", "readonly": "Read-only", + "hidden": "Hidden", "ok": "Ok" }, "headers": { @@ -2436,8 +2542,13 @@ "button": "Remove selected", "confirm_title": "Do you want to remove {number} {number, plural,\n one {entity}\n other {entities}\n}?", "confirm_partly_title": "Only {number} {number, plural,\n one {selected entity}\n other {selected entities}\n} can be removed.", - "confirm_text": "You should remove them from your Lovelace config and automations if they contain these entities.", + "confirm_text": "You should remove them from your dashboard config and automations if they contain these entities.", "confirm_partly_text": "You can only remove {removable} of the selected {selected} entities. Entities can only be removed when the integration is no longer providing the entities. Sometimes you have to restart Home Assistant before you can remove the entities of a removed integration. Are you sure you want to remove the removable entities?" + }, + "hide_selected": { + "button": "Hide selected", + "confirm_title": "Do you want to hide {number} {number, plural,\n one {entity}\n other {entities}\n}?", + "confirm_text": "Hidden entities will not be shown on your dashboard. Their history is still tracked and you can still interact with them with services." } } }, @@ -2693,120 +2804,6 @@ "stop_listening": "Stop listening", "message_received": "Message {id} received on {topic} at {time}:" }, - "ozw": { - "common": { - "zwave": "Z-Wave", - "node_id": "Node ID", - "ozw_instance": "OpenZWave Instance", - "instance": "Instance", - "controller": "Controller", - "network": "Network", - "wakeup_instructions": "Wake-up Instructions", - "query_stage": "Query Stage" - }, - "device_info": { - "zwave_info": "Z-Wave Info", - "stage": "Stage", - "node_failed": "Node Failed" - }, - "node_query_stages": { - "protocolinfo": "Obtaining basic Z-Wave capabilities of this node from the controller", - "probe": "Checking if the node is awake/alive", - "wakeup": "Setting up support for wake-up queues and messages", - "manufacturerspecific1": "Obtaining manufacturer and product ID codes from the node", - "nodeinfo": "Obtaining supported command classes from the node", - "nodeplusinfo": "Obtaining Z-Wave+ information from the node", - "manufacturerspecific2": "Obtaining additional manufacturer and product ID codes from the node", - "versions": "Obtaining information about firmware and command class versions", - "instances": "Obtaining details about what instances or channels a device supports", - "static": "Obtaining static values from the device", - "cacheload": "Loading information from the OpenZWave cache file. Battery nodes will stay at this stage until the node wakes up.", - "associations": "Refreshing association groups and memberships", - "neighbors": "Obtaining a list of the node's neighbors", - "session": "Obtaining infrequently changing values from the node", - "dynamic": "Obtaining frequently changing values from the node", - "configuration": "Obtaining configuration values from the node", - "complete": "Interview process is complete" - }, - "refresh_node": { - "button": "Refresh Node", - "title": "Refresh Node Information", - "complete": "Node Refresh Complete", - "description": "This will tell OpenZWave to re-interview a node and update the node's command classes, capabilities, and values.", - "battery_note": "If the node is battery powered, be sure to wake it before proceeding", - "wakeup_header": "Wake-up Instructions for", - "wakeup_instructions_source": "Wake-up instructions are sourced from the OpenZWave community device database.", - "start_refresh_button": "Start Refresh", - "refreshing_description": "Refreshing node information…", - "node_status": "Node Status", - "step": "Step" - }, - "network_status": { - "online": "Online", - "offline": "Offline", - "starting": "Starting", - "unknown": "Unknown", - "details": { - "driverallnodesqueried": "All nodes have been queried", - "driverallnodesqueriedsomedead": "All nodes have been queried. Some nodes were found dead", - "driverawakenodesqueries": "All awake nodes have been queried", - "driverremoved": "The driver has been removed", - "driverreset": "The driver has been reset", - "driverfailed": "Failed to connect to Z-Wave controller", - "driverready": "Initializing the Z-Wave controller", - "ready": "Ready to connect", - "stopped": "OpenZWave stopped", - "started": "Connected to MQTT", - "starting": "Connecting to MQTT", - "offline": "OZWDaemon offline" - } - }, - "navigation": { - "select_instance": "Select Instance", - "network": "Network", - "nodes": "Nodes", - "node": { - "dashboard": "Dashboard", - "config": "Config" - } - }, - "select_instance": { - "header": "Select an OpenZWave Instance", - "introduction": "You have more than one OpenZWave instance running. Which instance would you like to manage?", - "none_found": "We couldn't find an OpenZWave instance. If you believe this is incorrect, check your OpenZWave and MQTT setups and ensure that Home Assistant can communicate with your MQTT broker." - }, - "network": { - "header": "Network Management", - "introduction": "Manage network-wide functions.", - "node_count": "{count} nodes" - }, - "nodes_table": { - "id": "ID", - "manufacturer": "Manufacturer", - "model": "Model", - "query_stage": "Query Stage", - "zwave_plus": "Z-Wave Plus", - "failed": "Failed" - }, - "node": { - "button": "Node Details", - "not_found": "Node not found" - }, - "node_config": { - "header": "Node Configuration", - "introduction": "Manage the different configuration parameters for a Z-Wave node.", - "help_source": "Config parameter descriptions and help text are provided by the OpenZWave project.", - "wakeup_help": "Battery powered nodes must be awake to change their configuration. If the node is not awake, OpenZWave will attempt to update the node's configuration the next time it wakes up, which could be multiple hours (or days) later. Follow these steps to wake up your device:" - }, - "node_metadata": { - "product_manual": "Product Manual" - }, - "services": { - "add_node": "Add Node", - "remove_node": "Remove Node", - "cancel_command": "Cancel Command" - } - }, "zha": { "common": { "clusters": "Clusters", @@ -2900,96 +2897,6 @@ "unbind_button_help": "Unbind the selected group from the selected device clusters." } }, - "zwave": { - "description": "Manage your Z-Wave network", - "learn_more": "Learn more about Z-Wave", - "common": { - "value": "Value", - "instance": "Instance", - "index": "Index", - "unknown": "unknown", - "wakeup_interval": "Wake-up Interval" - }, - "migration": { - "zwave_js": { - "header": "Migrate to Z-Wave JS", - "introduction": "This integration is no longer maintained, and we advise you to move to the new Z-Wave JS integration. This wizard will help you migrate from the legacy Z-Wave integration to the new Z-Wave JS integration." - } - }, - "network_management": { - "header": "Z-Wave Network Management", - "introduction": "Run commands that affect the Z-Wave network. You won't get feedback on whether most commands succeeded, but you can check the OZW Log to try to find out." - }, - "node_management": { - "header": "Z-Wave Node Management", - "introduction": "Run Z-Wave commands that affect a single node. Pick a node to see a list of available commands.", - "nodes": "Nodes", - "nodes_hint": "Select node to view per-node options", - "entities": "Entities of this node", - "entity_info": "Entity Information", - "exclude_entity": "Exclude this entity from Home Assistant", - "pooling_intensity": "Polling intensity", - "node_protection": "Node protection", - "protection": "Protection", - "set_protection": "Set Protection", - "node_group_associations": "Node group associations", - "group": "Group", - "node_to_control": "Node to control", - "nodes_in_group": "Other nodes in this group:", - "max_associations": "Max Associations:", - "add_to_group": "Add to Group", - "remove_from_group": "Remove from Group", - "remove_broadcast": "Remove Broadcast" - }, - "ozw_log": { - "header": "OZW Log", - "introduction": "View the log. 0 is the minimum (loads entire log) and 1000 is the maximum. Load will show a static log and tail will auto update with the last specified number of lines of the log.", - "last_log_lines": "Number of last log lines", - "load": "Load", - "tail": "Tail" - }, - "network_status": { - "network_stopped": "Z-Wave Network Stopped", - "network_starting": "Starting Z-Wave Network…", - "network_starting_note": "This may take a while depending on the size of your network.", - "network_started": "Z-Wave Network Started", - "network_started_note_some_queried": "Awake nodes have been queried. Sleeping nodes will be queried when they wake.", - "network_started_note_all_queried": "All nodes have been queried." - }, - "node_config": { - "header": "Node Configuration Options", - "seconds": "seconds", - "set_wakeup": "Set Wake-up Interval", - "config_parameter": "Configuration Parameter", - "config_value": "Configuration Value", - "true": "True", - "false": "False", - "set_config_parameter": "Set Configuration Parameter" - }, - "values": { - "header": "Node Values" - }, - "services": { - "start_network": "Start Network", - "stop_network": "Stop Network", - "heal_network": "Heal Network", - "test_network": "Test Network", - "soft_reset": "Soft Reset", - "save_config": "Save Configuration", - "add_node_secure": "Add Node Secure", - "add_node": "Add Node", - "remove_node": "Remove Node", - "cancel_command": "Cancel Command", - "refresh_node": "Refresh Node", - "remove_failed_node": "Remove Failed Node", - "replace_failed_node": "Replace Failed Node", - "print_node": "Print Node", - "heal_node": "Heal Node", - "test_node": "Test Node", - "node_info": "Node Information", - "refresh_entity": "Refresh Entity" - } - }, "zwave_js": { "navigation": { "network": "Network", @@ -3281,7 +3188,7 @@ }, "unused_entities": { "title": "Unused entities", - "available_entities": "These are the entities that you have available, but are not in your Lovelace UI yet.", + "available_entities": "These are the entities that you have available, but are not in your dashboard yet.", "select_to_add": "Select the entities you want to add to a card and then click the add card button.", "state_icon": "State", "entity": "Entity", @@ -3292,9 +3199,9 @@ "no_data": "No unused entities found" }, "add_entities": { - "yaml_unsupported": "You cannot use this function when using Lovelace UI in YAML mode.", - "generated_unsupported": "You can only use this function when you have taken control of the Lovelace UI.", - "saving_failed": "Saving Lovelace UI configuration failed." + "yaml_unsupported": "You cannot use this function when using your dashboard in YAML mode.", + "generated_unsupported": "You can only use this function when you have taken control of your dashboard.", + "saving_failed": "Saving dashboard configuration failed." }, "views": { "confirm_delete": "Delete view?", @@ -3317,7 +3224,7 @@ "editor": { "header": "Edit UI", "menu": { - "open": "Open Lovelace UI menu", + "open": "Open dashboard menu", "raw_editor": "Raw configuration editor", "manage_dashboards": "Manage dashboards", "manage_resources": "Manage resources" @@ -3334,20 +3241,20 @@ "unsaved_changes": "Unsaved changes", "saved": "Saved", "reload": "Reload", - "lovelace_changed": "The Lovelace config was updated, do you want to load the updated config in the editor and lose your current changes?", - "confirm_remove_config_title": "Are you sure you want to remove your Lovelace UI configuration?", - "confirm_remove_config_text": "We will automatically generate your Lovelace UI views with your areas and devices if you remove your Lovelace UI configuration.", + "lovelace_changed": "Your dashboard was updated, do you want to load the updated config in the editor and lose your current changes?", + "confirm_remove_config_title": "Are you sure you want to remove your dashboard configuration?", + "confirm_remove_config_text": "We will automatically generate your dashboard views with your areas and devices if you remove your dashboard configuration.", "confirm_unsaved_changes": "You have unsaved changes, are you sure you want to exit?", "confirm_unsaved_comments": "Your configuration might contains comment(s), these will not be saved. Do you want to continue?", "error_parse_yaml": "Unable to parse YAML: {error}", "error_invalid_config": "Your configuration is not valid: {error}", "error_save_yaml": "Unable to save YAML: {error}", "error_remove": "Unable to remove configuration: {error}", - "resources_moved": "Resources should no longer be added to the Lovelace configuration but can be added in the Lovelace config panel." + "resources_moved": "Resources should no longer be added to the dashboard configuration but can be added in the dashboard config panel." }, "edit_lovelace": { - "header": "Title of your Lovelace UI", - "explanation": "This title is shown above all your views in the Lovelace UI.", + "header": "Title of your dashboard", + "explanation": "This title is shown above all your views in the dashboard.", "edit_title": "Edit title", "title": "Title" }, @@ -3409,13 +3316,13 @@ "suggest_card": { "header": "We created a suggestion for you", "create_own": "Pick different card", - "add": "Add to Lovelace UI" + "add": "Add to dashboard" }, "save_config": { - "header": "Take control of your Lovelace UI", - "para": "This dashboard is currently being maintained by Home Assistant. It is automatically updated when new entities or Lovelace UI components become available. If you take control, this dashboard will no longer be automatically updated. You can always create a new dashboard in configuration to play around with.", + "header": "Take control of your dashboard", + "para": "This dashboard is currently being maintained by Home Assistant. It is automatically updated when new entities or dashboard components become available. If you take control, this dashboard will no longer be automatically updated. You can always create a new dashboard in configuration to play around with.", "para_sure": "Are you sure you want to take control of your user interface?", - "yaml_mode": "You are using YAML mode for this dashboard, which means you cannot change your Lovelace config from the UI. If you want to manage this dashboard from the UI, remove 'mode: yaml' from your Lovelace configuration in 'configuration.yaml.'.", + "yaml_mode": "You are using YAML mode for this dashboard, which means you cannot change your dashboard config from the UI. If you want to manage this dashboard from the UI, remove 'mode: yaml' from your dashboard configuration in 'configuration.yaml.'.", "yaml_control": "To take control in YAML mode, create a YAML file with the name you specified in your config for this dashboard, or the default 'ui-lovelace.yaml'.", "yaml_config": "To help you start here is the current config of this dashboard:", "empty_config": "Start with an empty dashboard", @@ -3609,7 +3516,6 @@ "tap_action": "Tap Action", "title": "Title", "theme": "Theme", - "no_theme": "No theme", "unit": "Unit", "url": "URL", "state": "State", @@ -3728,7 +3634,7 @@ "starting": "Home Assistant is starting, not everything may be available yet" }, "changed_toast": { - "message": "The Lovelace UI configuration for this dashboard was updated. Refresh to see changes?" + "message": "Your dashboard was updated. Refresh to see changes?" }, "components": { "timestamp-display": { @@ -4527,7 +4433,8 @@ "privileged": "Supervisor is not privileged", "software": "Unsupported software detected", "source_mods": "Source modifications", - "systemd": "Systemd" + "systemd": "Systemd", + "systemd_resolved": "Systemd-Resolved" }, "unhealthy_reason": { "privileged": "Supervisor is not privileged", diff --git a/yarn.lock b/yarn.lock index 10cde4b7b8..7701804f4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2975,17 +2975,17 @@ __metadata: languageName: node linkType: hard -"@mdi/js@npm:6.5.95": - version: 6.5.95 - resolution: "@mdi/js@npm:6.5.95" - checksum: b1db7713d216c119f584bf973514a2f9d8f2e671e91bf19ce8e56cfa7a9843c0a060328e794507ac31f2bded1032123294f39ff8e987ea5acb2719ab522ef146 +"@mdi/js@npm:6.6.95": + version: 6.6.95 + resolution: "@mdi/js@npm:6.6.95" + checksum: 4cf8c48156f0e9ff67e4394cd428158bd164b1a6b7ca1aa70fc6a6aee91cfede9eba56720eb7d13fa57315ac636e9519a62dedd3cd2a9708aa11f2e3624ddbff languageName: node linkType: hard -"@mdi/svg@npm:6.5.95": - version: 6.5.95 - resolution: "@mdi/svg@npm:6.5.95" - checksum: 2d45221d042d52d54c85eaf672a5f3697ed5201607fa38a6e235ee2e60d1c3c25d456a284f19ce47b5f06418cacfee29e8fecf6580b8c28538fd26044becaf1a +"@mdi/svg@npm:6.6.95": + version: 6.6.95 + resolution: "@mdi/svg@npm:6.6.95" + checksum: 59b79db945847a3d981351418e0e7a457b831e09846fa751d44e80df8fb4cd19ef12bc889538ed2945d2638e522aa7ea5b1f97997e19dd68345f5d7bf5cad5e6 languageName: node linkType: hard @@ -3136,19 +3136,7 @@ __metadata: languageName: node linkType: hard -"@polymer/iron-dropdown@npm:^3.0.0-pre.26": - version: 3.0.1 - resolution: "@polymer/iron-dropdown@npm:3.0.1" - dependencies: - "@polymer/iron-behaviors": ^3.0.0-pre.26 - "@polymer/iron-overlay-behavior": ^3.0.0-pre.27 - "@polymer/neon-animation": ^3.0.0-pre.26 - "@polymer/polymer": ^3.0.0 - checksum: 2c1ba429c8f5553f8493f256691efa8a338e8c038c1102f482ecb612b61c079b5019f6c362aefb31b44d3429661152c1b6912408a69c67e9d6fff62914ad801f - languageName: node - linkType: hard - -"@polymer/iron-fit-behavior@npm:^3.0.0-pre.26, @polymer/iron-fit-behavior@npm:^3.1.0": +"@polymer/iron-fit-behavior@npm:^3.0.0-pre.26": version: 3.1.0 resolution: "@polymer/iron-fit-behavior@npm:3.1.0" dependencies: @@ -3295,17 +3283,6 @@ __metadata: languageName: node linkType: hard -"@polymer/neon-animation@npm:^3.0.0-pre.26": - version: 3.0.1 - resolution: "@polymer/neon-animation@npm:3.0.1" - dependencies: - "@polymer/iron-resizable-behavior": ^3.0.0-pre.26 - "@polymer/iron-selector": ^3.0.0-pre.26 - "@polymer/polymer": ^3.0.0 - checksum: c5ea5e1ef9f2017faaa5799ea108b26634dd7d986fe469369e629075efe382a5e5d4f9c537bacc77f9852453a2758c9f67e491d6ea5a1c4457f772bfdf06c707 - languageName: node - linkType: hard - "@polymer/paper-behaviors@npm:^3.0.0-pre.27": version: 3.0.1 resolution: "@polymer/paper-behaviors@npm:3.0.1" @@ -3318,25 +3295,6 @@ __metadata: languageName: node linkType: hard -"@polymer/paper-dropdown-menu@npm:^3.2.0": - version: 3.2.0 - resolution: "@polymer/paper-dropdown-menu@npm:3.2.0" - dependencies: - "@polymer/iron-a11y-keys-behavior": ^3.0.0-pre.26 - "@polymer/iron-form-element-behavior": ^3.0.0-pre.26 - "@polymer/iron-icon": ^3.0.0-pre.26 - "@polymer/iron-iconset-svg": ^3.0.0-pre.26 - "@polymer/iron-validatable-behavior": ^3.0.0-pre.26 - "@polymer/paper-behaviors": ^3.0.0-pre.27 - "@polymer/paper-input": ^3.1.0 - "@polymer/paper-menu-button": ^3.1.0 - "@polymer/paper-ripple": ^3.0.0-pre.26 - "@polymer/paper-styles": ^3.0.0-pre.26 - "@polymer/polymer": ^3.3.1 - checksum: dc7f6a8e3d449f37068ad5ee1d1c6d9037c9abd855ccc1d4e433d743c6378bb25af165ea86174edd1414887cf56a011ad0adda6d24dd55765e9458d49dc3e5e3 - languageName: node - linkType: hard - "@polymer/paper-icon-button@npm:^3.0.0-pre.26": version: 3.0.2 resolution: "@polymer/paper-icon-button@npm:3.0.2" @@ -3349,7 +3307,7 @@ __metadata: languageName: node linkType: hard -"@polymer/paper-input@npm:^3.0.0-pre.26, @polymer/paper-input@npm:^3.1.0, @polymer/paper-input@npm:^3.2.1": +"@polymer/paper-input@npm:^3.0.0-pre.26, @polymer/paper-input@npm:^3.2.1": version: 3.2.1 resolution: "@polymer/paper-input@npm:3.2.1" dependencies: @@ -3388,21 +3346,6 @@ __metadata: languageName: node linkType: hard -"@polymer/paper-menu-button@npm:^3.1.0": - version: 3.1.0 - resolution: "@polymer/paper-menu-button@npm:3.1.0" - dependencies: - "@polymer/iron-a11y-keys-behavior": ^3.0.0-pre.26 - "@polymer/iron-behaviors": ^3.0.0-pre.26 - "@polymer/iron-dropdown": ^3.0.0-pre.26 - "@polymer/iron-fit-behavior": ^3.1.0 - "@polymer/neon-animation": ^3.0.0-pre.26 - "@polymer/paper-styles": ^3.0.0-pre.26 - "@polymer/polymer": ^3.0.0 - checksum: 9243e104bac583189c6221f2df8dffeb331868cbf8084dd488cf2ddaba25987bfb3d4d2a9bd3168e6b49f28ba6b1b07ef7163fbfcf3af97978d34608e91cc605 - languageName: node - linkType: hard - "@polymer/paper-progress@npm:^3.0.0-pre.26": version: 3.0.1 resolution: "@polymer/paper-progress@npm:3.0.1" @@ -9105,15 +9048,14 @@ fsevents@^1.2.7: "@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.5.95 - "@mdi/svg": 6.5.95 + "@mdi/js": 6.6.95 + "@mdi/svg": 6.6.95 "@open-wc/dev-server-hmr": ^0.0.2 "@polymer/app-layout": ^3.1.0 "@polymer/iron-flex-layout": ^3.0.1 "@polymer/iron-icon": ^3.0.1 "@polymer/iron-input": ^3.0.1 "@polymer/iron-resizable-behavior": ^3.0.1 - "@polymer/paper-dropdown-menu": ^3.2.0 "@polymer/paper-input": ^3.2.1 "@polymer/paper-item": ^3.0.1 "@polymer/paper-listbox": ^3.0.1 @@ -9186,7 +9128,7 @@ fsevents@^1.2.7: gulp-rename: ^2.0.0 gulp-zopfli-green: ^3.0.1 hls.js: ^1.1.5 - home-assistant-js-websocket: ^6.0.1 + home-assistant-js-websocket: ^7.0.1 html-minifier: ^4.0.0 husky: ^1.3.1 idb-keyval: ^5.1.3 @@ -9240,7 +9182,6 @@ fsevents@^1.2.7: vis-network: ^8.5.4 vue: ^2.6.12 vue2-daterange-picker: ^0.5.1 - web-animations-js: ^2.3.2 webpack: ^5.55.1 webpack-cli: ^4.8.0 webpack-dev-server: ^4.3.0 @@ -9257,10 +9198,10 @@ fsevents@^1.2.7: languageName: unknown linkType: soft -"home-assistant-js-websocket@npm:^6.0.1": - version: 6.0.1 - resolution: "home-assistant-js-websocket@npm:6.0.1" - checksum: 566d6de6a4eb0e05ca434a45433cfe6fdd6b5cb2008e9a165709e08335df1c9b70903564c479ab8d48c6f5468a9784f47697192f9023170d2d86d43a461d6126 +"home-assistant-js-websocket@npm:^7.0.1": + version: 7.0.1 + resolution: "home-assistant-js-websocket@npm:7.0.1" + checksum: c9a87f11222571226adff43f022008d35df1f78799efae43e9a36f768eef10d21aed99886c905086c42c24d85d47c78e328c1be9593c117b397a18ee86b2fe64 languageName: node linkType: hard @@ -15703,13 +15644,6 @@ typescript@^4.4.3: languageName: node linkType: hard -"web-animations-js@npm:^2.3.2": - version: 2.3.2 - resolution: "web-animations-js@npm:2.3.2" - checksum: 194db111bb2f92c15100c33b63af320ccdc26066748e358a945b947c510216c78e0a1e2ae22fefbaacb585c8a0b41b62a1417d8b549636ee32e16f059bb488f2 - languageName: node - linkType: hard - "web-component-analyzer@npm:~1.1.1": version: 1.1.6 resolution: "web-component-analyzer@npm:1.1.6"