Compare commits

...

103 Commits

Author SHA1 Message Date
Paul Bottein 0b065799bf Add column option to grid section config
Add column density option to the grid section

Fix translations

Rename to grid density

Limit card size with the grid size

Rename function

Fix types
2024-10-14 19:40:42 +02:00
Wendelin ca94267c44 Fix tooltip firefox bug in persistent-notification-item (#22363) 2024-10-14 15:47:12 +02:00
renovate[bot] df1f26cee7 Update dependency magic-string to v0.30.12 (#22362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 12:00:18 +02:00
renovate[bot] 24a4e075e6 Update babel monorepo to v7.25.8 (#22355)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 11:48:23 +02:00
dependabot[bot] 43fcc6238e Bump actions/upload-artifact from 4.4.0 to 4.4.3 (#22359)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-14 09:29:29 +02:00
dependabot[bot] b40b96248b Bump actions/cache from 4.1.0 to 4.1.1 (#22358)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-14 09:17:16 +02:00
dependabot[bot] c7ac4c7490 Bump actions/checkout from 4.2.0 to 4.2.1 (#22360)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-14 09:17:03 +02:00
Abdulrasheed Abdulsalam 6cd8471b91 Fix: correct some typos in translation file (#22353) 2024-10-13 10:11:27 +00:00
Marc Mueller 940eaa26e0 Update build-system (#22348) 2024-10-13 07:41:03 +02:00
renovate[bot] 79e68ce125 Update dependency typescript to v5.6.3 (#22340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 11:13:22 +02:00
renovate[bot] e581d35432 Update formatjs monorepo (#22342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 09:14:19 +02:00
Paul Bottein d3d578e0f4 Hide fields section when all fields inside are filtered (#22277)
Hide field section when all fields inside are filtered
2024-10-11 21:52:44 +02:00
karwosts 79c71cbe48 Add sensor offset to time trigger UI (#21957)
* Add sensor offset to time trigger UI

* refactor long expression

* memoize data

* fix for trigger platform migration
2024-10-11 21:36:44 +02:00
karwosts 82b50a1c5d Refine automation action search with ignoreLocation (#22332) 2024-10-11 21:34:43 +02:00
karwosts 6cfda78aa1 Fix a case where developer-tools/action can get stuck in an error loop (#22334) 2024-10-11 20:37:49 +02:00
renovate[bot] f9ff938775 Update dependency @formatjs/intl-datetimeformat to v6.12.6 (#22335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-11 20:24:26 +02:00
Bram Kragten 778fcab90d Fix entity id setting on newly created scripts, handle update of enti… (#22272)
Fix entity id setting on newly created scripts, handle update of entity id
2024-10-11 13:13:17 +02:00
Wendelin 07e5aa30c6 Add hide completed option to hui-todo-list-card (#22323) 2024-10-11 08:58:40 +02:00
Paul Bottein 3f0ec03a14 Improve zigbee remove device dialog (#22276)
* Improve zigbee remove device dialog

* Fix translations
2024-10-11 06:40:15 +02:00
Alex Jurkiewicz 1bb871b9ac fix(script/bootstrap): Improve missing Yarn error (#22308) 2024-10-10 18:27:22 +03:00
Paul Bottein 0e8783fb01 Use default font for heading card (#22322) 2024-10-10 15:19:14 +00:00
Bram Kragten 1d88c4465b Bumped version to 20241010.0 2024-10-10 17:14:04 +02:00
Wendelin af2d575bf0 Fix ha-selector-action drag and drop (#22273)
* Fix ha-selector-action with removing memoize-one

* Fix array-move to update parent reference.

* Fix array-move if item is no array
2024-10-10 16:53:35 +02:00
Stefan Agner 92165d776a Fix command selection for OTBRs without dataset (#22318)
Typically, the Home Assistant OTBR integration makes sure that we
either setup or read the current dataset. However, in some cases,
e.g. when reinstalling the add-on, deleting the dataset, and starting
the add-on while keeping the OTBR config entry, the dataset is not
available and a new one is not being created (since the config entry
is not recreated).

Just support this particular case as well.

Fixes: #22306
2024-10-10 16:50:44 +02:00
renovate[bot] a8bbd8ab90 Update dependency @codemirror/commands to v6.7.0 (#22316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:21:29 +00:00
renovate[bot] 43ac9dbea7 Update vaadinWebComponents monorepo to v24.4.11 (#22315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:20:33 +00:00
renovate[bot] bba9eca4e9 Update dependency eslint-plugin-wc to v2.2.0 (#22310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:04:10 +02:00
renovate[bot] 40f65b1980 Update dependency del to v8 (#22311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-10 10:03:29 +02:00
karwosts 23a33b10a1 Allow override entity_id in more-info action (#22147) 2024-10-09 14:14:03 +02:00
Simon Lamon 67a93013c7 Revert "Fix drag and drop when using action and trigger selector" (#22296)
Revert "Fix drag and drop when using action and trigger selector (#22291)"

This reverts commit 99035cea8f.
2024-10-09 10:22:40 +00:00
Bram Kragten 1f838d7529 Update statistics issues from dev tools (#22286)
update statistics issues from dev tools
2024-10-09 09:29:30 +02:00
TJ Horner ffc0435144 Fix erroneous addition of service: key in ha-automation-action-play_media element (#22294)
fix: no longer erroneously set 'service' in ha-automation-action-play_media
2024-10-08 21:07:38 +00:00
David F. Mulcahey 5877d69c87 Fix ZHA group dashboard display on mobile (#22279) 2024-10-08 21:20:07 +02:00
Paul Bottein 99035cea8f Fix drag and drop when using action and trigger selector (#22291)
* Fix drag and drop when using action selector

* Fix drag and drop when using trigger selector
2024-10-08 21:04:45 +02:00
__JosephAbbey 1b441a7eec Add support for relative start and end time displays in state-display (#22249)
* Add support for relative start and end time displays in state-display

* Add support for sun attributes as well

* Improve state-display code for relative-time

---------

Co-authored-by: Wendelin <w@pe8.at>
2024-10-08 10:04:16 +02:00
Paul Bottein ad49e9f7b0 Add minimal size for badges and cards in edit mode (#22271) 2024-10-07 17:33:47 +02:00
Petar Petrov e32b15ede2 Hide service dropdown for predefined actions in automations (#22275)
Hide service dropdown for predefined actions
2024-10-07 15:49:44 +02:00
Wendelin a35b4376ea Fix unused entities view (#22274)
Fix compute-unused-entities when using sections
2024-10-07 15:28:14 +02:00
Simon Lamon 619f9f76ee Fixup service/action when entity is picked in activate scene (#22259)
Fixup service/action when entity is picked
2024-10-07 09:23:48 +02:00
dependabot[bot] f771bc10db Bump actions/cache from 4.0.2 to 4.1.0 (#22270)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 08:44:00 +02:00
renovate[bot] b8889a1183 Update dependency eslint-plugin-import to v2.31.0 (#22260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-06 10:47:08 +00:00
renovate[bot] eb6b45eaed Update babel monorepo to v7.25.7 (#22250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 20:11:42 +02:00
renovate[bot] 31a748ed93 Update dependency date-fns-tz to v3.2.0 (#22209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 21:38:25 +02:00
Bram Kragten 0110bdd24a Fix and update step flow create (#22223)
* Fix and update step flow create

* cleanup
2024-10-04 14:13:21 +02:00
Bram Kragten 365b712976 Add temporary logging to webrtc player (#22213)
* add temporary logging to webrtc player

* Update ha-web-rtc-player.ts

* Update ha-web-rtc-player.ts

* Update ha-web-rtc-player.ts
2024-10-03 21:01:21 +02:00
Paul Bottein 7d97dbe15b Fix potential undefined select element in color picker (#22212) 2024-10-03 11:31:36 +02:00
Paul Bottein 8bc0ea5a0b Update heading entity schema to allow empty entity id (#22211) 2024-10-03 11:15:13 +02:00
Adam Kapos 44948a3474 Disable backdrop filter on Heading Card (#22204) 2024-10-03 09:45:29 +02:00
Robert Resch bc51b53b4a Use camera ws endpoint to get WebRTC config (#22009) 2024-10-03 09:19:49 +02:00
Bram Kragten 67217b9dd0 Bumped version to 20241002.2 2024-10-02 16:42:46 +02:00
Bram Kragten 487795b7c4 handle unknown state for update voice assitant (#22196)
* handle unknown state for update voice assitant

* Update voice-assistant-setup-step-update.ts
2024-10-02 16:42:27 +02:00
Petar Petrov a30e0d33f9 Handle exceptions when subscribing from the event dev tool (#22191)
* Handle exceptions when subscribing from the event dev tool

* use ha-alert for the error msg

* import ha-alert element

* use undefined instead of null to align with the rest of the code base
2024-10-02 16:34:28 +02:00
Bram Kragten 0c1b8abe03 Fix hassio entrypoint (#22194) 2024-10-02 14:20:53 +00:00
Paul Bottein ce9c5149d5 Use heading card in demo dashboard (#22193) 2024-10-02 14:13:26 +00:00
Bram Kragten adbcdc62eb Alert user when auto update is enabled instead of hiding the button (#22187) 2024-10-02 15:41:15 +02:00
Paul Bottein faf872bfb8 Simplify create automation from device dialog (#22190)
* Simplify automation dialog

* Fix translations

* Auto expand trigger action and condition

* Improve wording

* Expand all

* Remove unused translations
2024-10-02 13:13:21 +00:00
Stefan Agner fe0fb2382a Allow to transfer all Thread datasets with TLV (#22183)
* Allow to transfer all Thread datasets with TLV

This commit allows to transfer all Thread datasets with TLV. Since
PR #22022 the preferred dataset is transmitted when using Matter
external commissioning. This commit makes the Thread configuration
dialog to have feature parity.

* Drop preferred border agent id as additional metric for default router

We always have the extended address, so use this as primary and only
metric which router is the default. The preferred border agent id gets
updated best effort.

Also use isDefaultRouter consistently in the code.
2024-10-02 15:06:06 +02:00
Bram Kragten cdd29295e5 Bumped version to 20241002.1 2024-10-02 13:37:47 +02:00
karwosts f7532f3476 Devtools statistics - new style, multi-select, & multi-delete (#21813)
* feat: auto-fix statistics

* statistics multi-select and multi-fix

* unused css

* Change multi action to clear, fixes

* Update developer-tools-statistics.ts

* update translations

* Add select all issues option

* Update en.json

* Update developer-tools-statistics.ts

---------

Co-authored-by: Muka Schultze <samuelschultze@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-10-02 13:21:04 +02:00
Bram Kragten c8930cec87 Bumped version to 20241002.0 2024-10-02 09:50:01 +02:00
Bram Kragten f9c336890d Await removal of statistics when fixing (#22167)
* Await removal of statistics when fixing

* refactor

* translations
2024-10-02 09:24:46 +02:00
Bram Kragten c721de109f Put rename entities in expandable when renaming device (#22182)
* Put rename entities in expandable when renaming device

* Update ha-config-device-page.ts

* Update src/panels/config/devices/ha-config-device-page.ts

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-10-02 07:19:05 +00:00
renovate[bot] 1c95e8d6ec Update vaadinWebComponents monorepo to v24.4.10 (#22180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 16:34:51 +00:00
Bram Kragten 57cf2c1341 Update update entity in voice flow (#22178) 2024-10-01 18:34:18 +02:00
renovate[bot] f7d5c5f850 Update dependency @codemirror/view to v6.34.1 (#22179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 16:33:42 +00:00
renovate[bot] 470f5127f4 Update dependency @types/color-name to v2 (#22157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 18:22:13 +02:00
Paul Bottein 34e361601a Fix display elements field in heading badge editor (#22177)
* Fix display elements field in heading badge editor

* Update src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts
2024-10-01 16:58:00 +02:00
Paul Bottein 70d6cce8f8 Add support for custom color display in color picker (#22174)
Add support for custom color in color picker
2024-10-01 15:43:31 +02:00
Paul Bottein f9814f35d1 Don't handle UI editor event when using yaml editor (#22176) 2024-10-01 15:11:54 +02:00
Paul Bottein f30603753e Use radio buttons for heading style (#22173) 2024-10-01 14:26:51 +02:00
karwosts ce9993fd36 Use finishes_at in timer remaining calculation (#22169)
* Use finishes_at in timer remaining calculation

* lint

* fix test
2024-10-01 14:05:01 +02:00
Bram Kragten 4c2044e70a Bumped version to 20240930.0 2024-09-30 17:11:52 +02:00
Darren Griffin 7f96c1fbe1 Add OHF logo to README (#22165) 2024-09-30 17:10:45 +02:00
Paul Bottein 75e24780c1 Use dash for unknown and unavailable state in heading entity (#22163)
* Use dash for unknown and unavailable state in heading entity

* Update src/state-display/state-display.ts

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-09-30 15:52:20 +02:00
Bram Kragten 95580bc4c0 Fix min width of checkbox column in data table (#22162)
fix min width of checkbox column in data table
2024-09-30 09:09:08 +00:00
Paul Bottein 175f68e0cf Allow to add name in heading entity badge state content (#22161) 2024-09-30 09:07:25 +00:00
Bart Mesuere b6efedfc8d Improve the accessibility of the default colors used for graphs (#21839)
* Update the first 10 colors to match the Observable10 scheme

* Add darker and lighter variants
2024-09-30 10:59:13 +02:00
Wendelin 23c21a35d8 Fix script rename name placeholder (#22160) 2024-09-30 07:43:59 +00:00
Simon Lamon 9c7324298b Remove floor context (#22143)
* Remove floor context

* Fixup gallery
2024-09-30 09:33:08 +02:00
Matthias Alphart e92be566a0 Handle falsy value in ha-yaml-editor (object selector) (#22142)
* Handle falsy value in ha-yaml-editor (object selector)

* handle explicit `null`
2024-09-30 09:28:17 +02:00
dependabot[bot] 4e96ad5f28 Bump actions/checkout from 4.1.7 to 4.2.0 (#22159)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 09:07:50 +02:00
renovate[bot] f64a1500af Update dependency webpack to v5.95.0 (#22150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 21:23:29 +02:00
renovate[bot] c9e8619c04 Update dependency @codemirror/view to v6.34.0 (#22144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 21:23:21 +02:00
Bram Kragten 7ab1133b45 Implement missing function for password field 2024-09-27 23:42:44 +02:00
Bram Kragten 77abfd3e61 voice setup tweaks 2024-09-27 23:42:09 +02:00
Bram Kragten d7aaa41aa4 Add missing voice assistant select action logic (#22139) 2024-09-27 14:40:55 -04:00
Aindriú Mac Giolla Eoin 8223f6b155 Update translationMetadata.json - Added Irish language code (#21898)
Added language code for Irish, native name Gaeilge
2024-09-27 18:12:44 +02:00
renovate[bot] 435eae77fa Update dependency rollup to v2.79.2 [SECURITY] (#22071)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-27 15:29:29 +00:00
Paul Bottein ead54e445f Reuse flatten logic for trigger ids condition (#22136) 2024-09-27 17:18:06 +02:00
Bram Kragten 7ee5db2be5 Bumped version to 20240927.0 2024-09-27 17:15:57 +02:00
Bram Kragten fef6f0ac94 migrate nested triggers too (#22135) 2024-09-27 15:13:05 +00:00
Bram Kragten 7a60763786 Voice setup feedback (#22134)
* Voice setup feedback

* Update voice-assistant-setup-step-check.ts
2024-09-27 16:56:38 +02:00
Paul Bottein 94e321a364 Add UI support for trigger list (#22133)
* Add UI support for trigger list

* Update gallery

* Fix gallery
2024-09-27 16:56:22 +02:00
Bram Kragten 1c12c2b714 Fix codemirror fold for empty lines (#22130) 2024-09-27 14:18:48 +02:00
Paul Bottein 442a8f11a7 Improve heading card style and add theme variables (#22129)
improve heading card style and add theme variables
2024-09-27 13:45:18 +02:00
Bram Kragten 4e8b58cd6c Add password field element (#22121)
* Add password field element

* Update ha-password-field.ts
2024-09-27 12:34:28 +02:00
Paul Bottein a92dab46c2 Allow different types of heading badges (#22109)
* Allow different type of heading item

* Update editor

* Migrate entities to items

* Rename support for string entity

* Refactor

* Rename to badges and add error state

* Update font weight

* Feedback

* Feedback
2024-09-27 12:33:15 +02:00
Joakim Sørensen 468660d235 Adjust username handling in the cloud panel register and login flows (#22118)
* Use lowercase when registering

* Fallback to lowercase username if usernotfound is recieved

* Adjust resend

* handle reset password

* limit with else

* return early
2024-09-27 12:31:48 +02:00
Wendelin c721afa137 Fix matter device actions (#22117)
* Fix matter device actions when matter integration loads forever

* Fix matter device-actions types path

* Move getMatterDeviceActions inside getDeviceActions in device page
2024-09-27 09:37:07 +00:00
Paul Bottein ac9654c1de Add heading card when creating a new view (#22123) 2024-09-27 09:19:19 +00:00
Wendelin 570ad38bac Fix automation trigger condition and triggers description (#22122)
* Fix config.triggers in automation-contition-trigger

* Fix config.triggers for automation triggers description
2024-09-27 09:10:33 +00:00
Erik Montnemery e778a9aa1d Improve statistics issues (#22110) 2024-09-27 11:05:30 +02:00
selvalt7 49576189af Use localizeValue in ha-form-expandable and ha-form-grid (#22114)
Pass localizeValue to ha-form-expandable and ha-form-grid
2024-09-27 10:00:36 +02:00
149 changed files with 4908 additions and 2963 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
with:
ref: dev
@@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
with:
ref: master
+7 -7
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.0.2
uses: actions/cache@v4.1.1
with:
path: |
node_modules/.cache/prettier
@@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.4.3
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.0.4
with:
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.4.3
with:
name: supervisor-bundle-stats
path: build/stats/*.json
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
with:
ref: dev
@@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
with:
ref: master
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.0.4
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
- name: Setup Node
uses: actions/setup-node@v4.0.4
+3 -3
View File
@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.4.3
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.4.3
with:
name: translations
path: translations.tar.gz
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.1
- name: Upload Translations
run: |
+2
View File
@@ -27,3 +27,5 @@ A complete guide can be found at the following [link](https://www.home-assistant
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
[![Home Assistant - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/home-assistant.png)](https://www.openhomefoundation.org/)
+26
View File
@@ -111,6 +111,16 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Living room Temperature",
},
},
"sensor.living_room_humidity": {
entity_id: "sensor.living_room_humidity",
state: "57",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Living room Humidity",
},
},
"sensor.outdoor_temperature": {
entity_id: "sensor.outdoor_temperature",
state: "10.5",
@@ -189,6 +199,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
supported_features: 32,
},
},
"binary_sensor.kitchen_motion": {
entity_id: "light.kitchen_motion",
state: "on",
attributes: {
device_class: "motion",
friendly_name: "Kitchen motion",
},
},
"light.worktop_spotlights": {
entity_id: "light.worktop_spotlights",
state: "off",
@@ -423,6 +441,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
supported_features: 64063,
},
},
"switch.in_meeting": {
entity_id: "switch.in_meeting",
state: "on",
attributes: {
icon: "mdi:laptop-account",
friendly_name: "In a meeting",
},
},
"sensor.standing_desk_height": {
entity_id: "sensor.standing_desk_height",
state: "72",
+97 -16
View File
@@ -30,12 +30,36 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
? []
: [
{
title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
cards: [{ type: "custom:ha-demo-card" }],
cards: [
{
type: "heading",
heading: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
},
{ type: "custom:ha-demo-card" },
],
},
]),
{
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.living_room"
),
icon: "mdi:sofa",
badges: [
{
type: "entity",
entity: "sensor.living_room_temperature",
color: "red",
},
{
type: "entity",
entity: "sensor.living_room_humidity",
color: "indigo",
},
],
},
{
type: "tile",
entity: "light.floor_lamp",
@@ -54,13 +78,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
type: "tile",
entity: "light.bar_lamp",
},
{
graph: "line",
type: "sensor",
entity: "sensor.living_room_temperature",
detail: 1,
name: "Temperature",
},
{
type: "tile",
entity: "cover.living_room_garden_shutter",
@@ -71,11 +88,25 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
entity: "media_player.living_room_nest_mini",
},
],
title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.kitchen"
),
icon: "mdi:fridge",
badges: [
{
type: "entity",
entity: "binary_sensor.kitchen_motion",
show_state: false,
color: "blue",
},
],
},
{
type: "tile",
entity: "cover.kitchen_shutter",
@@ -106,11 +137,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
entity: "media_player.kitchen_nest_audio",
},
],
title: `👩‍🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.energy"
),
icon: "mdi:transmission-tower",
},
{
type: "tile",
entity: "binary_sensor.tesla_wall_connector_vehicle_connected",
@@ -148,11 +185,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "dark-grey",
},
],
title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.climate"
),
icon: "mdi:thermometer",
},
{
type: "tile",
entity: "sun.sun",
@@ -185,16 +228,38 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
state_content: ["preset_mode", "current_temperature"],
},
],
title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.study"
),
icon: "mdi:desk-lamp",
badges: [
{
type: "entity",
entity: "switch.in_meeting",
state: "on",
state_content: "name",
visibility: [
{
condition: "state",
state: "on",
entity: "switch.in_meeting",
},
],
},
],
},
{
type: "tile",
entity: "cover.study_shutter",
name: "Shutter",
},
{
type: "tile",
entity: "light.study_spotlights",
@@ -211,12 +276,23 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "brown",
icon: "mdi:desk",
},
{
type: "tile",
entity: "switch.in_meeting",
name: "Meeting mode",
},
],
title: `🧑‍💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.outdoor"
),
icon: "mdi:tree",
},
{
type: "tile",
entity: "light.outdoor_light",
@@ -246,11 +322,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
name: "Illuminance",
},
],
title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.updates"
),
icon: "mdi:update",
},
{
type: "tile",
entity: "automation.home_assistant_auto_update",
@@ -276,7 +358,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
icon: "mdi:home-assistant",
},
],
title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`,
},
],
},
+9
View File
@@ -0,0 +1,9 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfig = (hass: MockHomeAssistant) => {
hass.mockWS("validate_config", () => ({
actions: { valid: true },
conditions: { valid: true },
triggers: { valid: true },
}));
};
+6
View File
@@ -0,0 +1,6 @@
import { Tag } from "../../../src/data/tag";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockTags = (hass: MockHomeAssistant) => {
hass.mockWS("tag/list", () => [{ id: "my-tag", name: "My Tag" }] as Tag[]);
};
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action">
<span>
${this._action
? describeAction(this.hass, [], [], [], this._action)
? describeAction(this.hass, [], [], this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, [], [], [], conf as any)}</span>
<span>${describeAction(this.hass, [], [], conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`
@@ -58,6 +58,12 @@ const triggers = [
command: ["Turn on the lights", "Turn the lights on"],
},
{ trigger: "event", event_type: "homeassistant_started" },
{
triggers: [
{ trigger: "state", entity_id: "light.kitchen", to: "on" },
{ trigger: "state", entity_id: "light.kitchen", to: "off" },
],
},
];
const initialTrigger: Trigger = {
@@ -8,6 +8,9 @@ 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 { mockConfig } from "../../../../demo/src/stubs/config";
import { mockTags } from "../../../../demo/src/stubs/tags";
import { mockAuth } from "../../../../demo/src/stubs/auth";
import type { Trigger } from "../../../../src/data/automation";
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
@@ -26,6 +29,7 @@ import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{
@@ -116,6 +120,10 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
},
],
},
{
name: "Trigger list",
triggers: [{ ...HaTriggerList.defaultConfig }],
},
];
@customElement("demo-automation-editor-trigger")
@@ -135,6 +143,9 @@ export class DemoAutomationEditorTrigger extends LitElement {
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
mockConfig(hass);
mockTags(hass);
mockAuth(hass);
}
protected render(): TemplateResult {
@@ -15,6 +15,7 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
import "../../../src/components/ha-password-field";
import "../../../src/components/ha-radio";
import type { HaRadio } from "../../../src/components/ha-radio";
import {
@@ -261,23 +262,21 @@ export class SupervisorBackupContent extends LitElement {
: ""}
${this.backupHasPassword
? html`
<ha-textfield
<ha-password-field
.label=${this._localize("password")}
type="password"
name="backupPassword"
.value=${this.backupPassword}
@change=${this._handleTextValueChanged}
>
</ha-textfield>
</ha-password-field>
${!this.backup
? html`<ha-textfield
? html`<ha-password-field
.label=${this._localize("confirm_password")}
type="password"
name="confirmBackupPassword"
.value=${this.confirmBackupPassword}
@change=${this._handleTextValueChanged}
>
</ha-textfield>`
</ha-password-field>`
: ""}
`
: ""}
@@ -13,10 +13,12 @@ import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-password-field";
import "../../../../src/components/ha-radio";
import "../../../../src/components/ha-textfield";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
AccessPoints,
@@ -34,7 +36,6 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
import type { HaTextField } from "../../../../src/components/ha-textfield";
const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -246,9 +247,8 @@ export class DialogHassioNetwork
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<ha-textfield
<ha-password-field
class="flex-auto"
type="password"
id="psk"
.label=${this.supervisor.localize(
"dialog.network.wifi_password"
@@ -256,7 +256,7 @@ export class DialogHassioNetwork
version="wifi"
@change=${this._handleInputValueChangedWifi}
>
</ha-textfield>
</ha-password-field>
`
: ""}
`
+2 -1
View File
@@ -13,10 +13,11 @@
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
} else {
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
}
})();
+27 -27
View File
@@ -25,24 +25,24 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.25.6",
"@babel/runtime": "7.25.7",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/commands": "6.6.2",
"@codemirror/commands": "6.7.0",
"@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.33.0",
"@codemirror/view": "6.34.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8",
"@formatjs/intl-datetimeformat": "6.13.0",
"@formatjs/intl-displaynames": "6.6.9",
"@formatjs/intl-getcanonicallocales": "2.3.0",
"@formatjs/intl-listformat": "7.5.7",
"@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.2.14",
"@formatjs/intl-relativetimeformat": "11.2.14",
"@formatjs/intl-listformat": "7.5.8",
"@formatjs/intl-locale": "4.0.1",
"@formatjs/intl-numberformat": "8.11.0",
"@formatjs/intl-pluralrules": "5.2.15",
"@formatjs/intl-relativetimeformat": "11.2.15",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -89,8 +89,8 @@
"@polymer/polymer": "3.5.1",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.9",
"@vaadin/vaadin-themable-mixin": "24.4.9",
"@vaadin/combo-box": "24.4.11",
"@vaadin/vaadin-themable-mixin": "24.4.11",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -104,7 +104,7 @@
"core-js": "3.38.1",
"cropperjs": "1.6.2",
"date-fns": "4.1.0",
"date-fns-tz": "3.1.3",
"date-fns-tz": "3.2.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
@@ -114,7 +114,7 @@
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.14",
"intl-messageformat": "10.6.0",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -151,12 +151,12 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.25.2",
"@babel/core": "7.25.8",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.24.7",
"@babel/plugin-transform-runtime": "7.25.4",
"@babel/preset-env": "7.25.4",
"@babel/preset-typescript": "7.24.7",
"@babel/plugin-proposal-decorators": "7.25.7",
"@babel/plugin-transform-runtime": "7.25.7",
"@babel/preset-env": "7.25.8",
"@babel/preset-typescript": "7.25.7",
"@bundle-stats/plugin-webpack-filter": "4.15.1",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.7.0",
@@ -172,7 +172,7 @@
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.17",
"@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "1.1.4",
"@types/color-name": "2.0.0",
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
@@ -195,17 +195,17 @@
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.1",
"del": "7.1.0",
"del": "8.0.0",
"eslint": "8.57.1",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.9",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "2.1.1",
"eslint-plugin-wc": "2.2.0",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
"glob": "11.0.0",
@@ -222,14 +222,14 @@
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"magic-string": "0.30.11",
"magic-string": "0.30.12",
"map-stream": "0.0.7",
"mocha": "10.5.0",
"object-hash": "3.0.0",
"open": "10.1.0",
"pinst": "3.0.0",
"prettier": "3.3.3",
"rollup": "2.79.1",
"rollup": "2.79.2",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0",
@@ -240,8 +240,8 @@
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.6.2",
"webpack": "5.94.0",
"typescript": "5.6.3",
"webpack": "5.95.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0",
"webpack-manifest-plugin": "5.0.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+2 -2
View File
@@ -1,10 +1,10 @@
[build-system]
requires = ["setuptools~=68.0", "wheel~=0.40.0"]
requires = ["setuptools~=75.1"]
build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20240926.0"
version = "20241010.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
+5 -1
View File
@@ -18,5 +18,9 @@ if [[ -n "$DEVCONTAINER" ]]; then
fi
fi
if ! command -v yarn &> /dev/null; then
echo "Error: yarn not found. Please install it following the official instructions: https://yarnpkg.com/getting-started/install" >&2
exit 1
fi
# Install node modules
yarn install
yarn install
+30 -30
View File
@@ -1,36 +1,36 @@
import { theme2hex } from "./convert-color";
export const COLORS = [
"#44739e",
"#984ea3",
"#00d2d5",
"#ff7f00",
"#af8d00",
"#7f80cd",
"#b3e900",
"#c42e60",
"#a65628",
"#f781bf",
"#8dd3c7",
"#bebada",
"#fb8072",
"#80b1d3",
"#fdb462",
"#fccde5",
"#bc80bd",
"#ffed6f",
"#c4eaff",
"#cf8c00",
"#1b9e77",
"#d95f02",
"#e7298a",
"#e6ab02",
"#a6761d",
"#0097ff",
"#00d067",
"#f43600",
"#4ba93b",
"#5779bb",
"#4269d0",
"#f4bd4a",
"#ff725c",
"#6cc5b0",
"#a463f2",
"#ff8ab7",
"#9c6b4e",
"#97bbf5",
"#01ab63",
"#9498a0",
"#094bad",
"#c99000",
"#d84f3e",
"#49a28f",
"#048732",
"#d96895",
"#8043ce",
"#7599d1",
"#7a4c31",
"#74787f",
"#6989f4",
"#ffd444",
"#ff957c",
"#8fe9d3",
"#62cc71",
"#ffadda",
"#c884ff",
"#badeff",
"#bf8b6d",
"#b6bac2",
"#927acc",
"#97ee3f",
"#bf3947",
+18 -5
View File
@@ -20,6 +20,15 @@ function findNestedItem(
}, obj);
}
function updateNestedItem(obj: any, path: ItemPath): any {
const lastKey = path.pop()!;
const parent = findNestedItem(obj, path);
parent[lastKey] = Array.isArray(parent[lastKey])
? [...parent[lastKey]]
: [parent[lastKey]];
return obj;
}
export function nestedArrayMove<A>(
obj: A,
oldIndex: number,
@@ -27,14 +36,18 @@ export function nestedArrayMove<A>(
oldPath?: ItemPath,
newPath?: ItemPath
): A {
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
if (oldPath) {
newObj = updateNestedItem(newObj, [...oldPath]);
}
if (newPath) {
newObj = updateNestedItem(newObj, [...newPath]);
}
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
if (!Array.isArray(from) || !Array.isArray(to)) {
return obj;
}
const item = from.splice(oldIndex, 1)[0];
to.splice(newIndex, 0, item);
@@ -204,6 +204,29 @@ export class HaDataTable extends LitElement {
this._checkedRowsChanged();
}
public select(ids: string[], clear?: boolean): void {
if (clear) {
this._checkedRows = [];
}
ids.forEach((id) => {
const row = this._filteredData.find((data) => data[this.id] === id);
if (row?.selectable !== false && !this._checkedRows.includes(id)) {
this._checkedRows.push(id);
}
});
this._checkedRowsChanged();
}
public unselect(ids: string[]): void {
ids.forEach((id) => {
const index = this._checkedRows.indexOf(id);
if (index > -1) {
this._checkedRows.splice(index, 1);
}
});
this._checkedRowsChanged();
}
public connectedCallback() {
super.connectedCallback();
if (this._filteredData.length) {
@@ -1011,6 +1034,7 @@ export class HaDataTable extends LitElement {
/* @noflip */
padding-inline-end: initial;
width: 60px;
min-width: 60px;
}
.mdc-data-table__table {
+29 -4
View File
@@ -1,6 +1,6 @@
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
@@ -8,8 +8,9 @@ import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeKeys } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import "./ha-md-divider";
import "./ha-select";
import type { HaSelect } from "./ha-select";
@customElement("ha-color-picker")
export class HaColorPicker extends LitElement {
@@ -32,7 +33,17 @@ export class HaColorPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
_valueSelected(ev) {
@query("ha-select") private _select?: HaSelect;
connectedCallback(): void {
super.connectedCallback();
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
this._select?.layoutOptions();
}
private _valueSelected(ev) {
ev.stopPropagation();
if (!this.isConnected) return;
const value = ev.target.value;
this.value = value === this.defaultColor ? undefined : value;
fireEvent(this, "value-changed", {
@@ -41,7 +52,13 @@ export class HaColorPicker extends LitElement {
}
render() {
const value = this.value || this.defaultColor;
const value = this.value || this.defaultColor || "";
const isCustom = !(
THEME_COLORS.has(value) ||
value === "none" ||
value === "state"
);
return html`
<ha-select
@@ -110,6 +127,14 @@ export class HaColorPicker extends LitElement {
</ha-list-item>
`
)}
${isCustom
? html`
<ha-list-item .value=${value} graphic="icon">
${value}
<span slot="graphic">${this.renderColorCircle(value)}</span>
</ha-list-item>
`
: nothing}
</ha-select>
`;
}
@@ -30,6 +30,10 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
options?: { path?: string[] }
) => string;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
private _renderDescription() {
const description = this.computeHelper?.(this.schema);
return description ? html`<p>${description}</p>` : nothing;
@@ -86,6 +90,7 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
.disabled=${this.disabled}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
.localizeValue=${this.localizeValue}
></ha-form>
</div>
</ha-expansion-panel>
+5
View File
@@ -35,6 +35,10 @@ export class HaFormGrid extends LitElement implements HaFormElement {
schema: HaFormSchema
) => string;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
public async focus() {
await this.updateComplete;
this.renderRoot.querySelector("ha-form")?.focus();
@@ -65,6 +69,7 @@ export class HaFormGrid extends LitElement implements HaFormElement {
.disabled=${this.disabled}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
.localizeValue=${this.localizeValue}
></ha-form>
`
)}
+1
View File
@@ -163,6 +163,7 @@ export class HaForm extends LitElement implements HaFormElement {
localize: this.hass?.localize,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
localizeValue: this.localizeValue,
context: this._generateContext(item),
...this.getFormProperties(),
})}
+58
View File
@@ -0,0 +1,58 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
type HeadingBadgeType = "text" | "button";
@customElement("ha-heading-badge")
export class HaBadge extends LitElement {
@property() public type: HeadingBadgeType = "text";
protected render() {
return html`
<div
class="heading-badge"
role=${ifDefined(this.type === "button" ? "button" : undefined)}
tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
>
<slot name="icon"></slot>
<slot></slot>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
color: var(--secondary-text-color);
}
[role="button"] {
cursor: pointer;
}
.heading-badge {
display: flex;
flex-direction: row;
white-space: nowrap;
align-items: center;
gap: 3px;
font-family: Roboto;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.1px;
--mdc-icon-size: 14px;
}
::slotted([slot="icon"]) {
--ha-icon-display: block;
color: var(--icon-color, inherit);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-heading-badge": HaBadge;
}
}
+185
View File
@@ -0,0 +1,185 @@
import { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { LitElement, css, html } from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-password-field")
export class HaPasswordField extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public invalid?: boolean;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public icon = false;
@property({ type: Boolean }) public iconTrailing = false;
@property() public autocomplete?: string;
@property() public autocorrect?: string;
@property({ attribute: "input-spellcheck" })
public inputSpellcheck?: string;
@property({ type: String }) value = "";
@property({ type: String }) placeholder = "";
@property({ type: String }) label = "";
@property({ type: Boolean, reflect: true }) disabled = false;
@property({ type: Boolean }) required = false;
@property({ type: Number }) minLength = -1;
@property({ type: Number }) maxLength = -1;
@property({ type: Boolean, reflect: true }) outlined = false;
@property({ type: String }) helper = "";
@property({ type: Boolean }) validateOnInitialRender = false;
@property({ type: String }) validationMessage = "";
@property({ type: Boolean }) autoValidate = false;
@property({ type: String }) pattern = "";
@property({ type: Number }) size: number | null = null;
@property({ type: Boolean }) helperPersistent = false;
@property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter =
false;
@property({ type: Boolean }) endAligned = false;
@property({ type: String }) prefix = "";
@property({ type: String }) suffix = "";
@property({ type: String }) name = "";
@property({ type: String, attribute: "input-mode" })
inputMode!: string;
@property({ type: Boolean }) readOnly = false;
@property({ type: String }) autocapitalize = "";
@state() private _unmaskedPassword = false;
@query("ha-textfield") private _textField!: HaTextField;
protected render() {
return html`<ha-textfield
.invalid=${this.invalid}
.errorMessage=${this.errorMessage}
.icon=${this.icon}
.iconTrailing=${this.iconTrailing}
.autocomplete=${this.autocomplete}
.autocorrect=${this.autocorrect}
.inputSpellcheck=${this.inputSpellcheck}
.value=${this.value}
.placeholder=${this.placeholder}
.label=${this.label}
.disabled=${this.disabled}
.required=${this.required}
.minLength=${this.minLength}
.maxLength=${this.maxLength}
.outlined=${this.outlined}
.helper=${this.helper}
.validateOnInitialRender=${this.validateOnInitialRender}
.validationMessage=${this.validationMessage}
.autoValidate=${this.autoValidate}
.pattern=${this.pattern}
.size=${this.size}
.helperPersistent=${this.helperPersistent}
.charCounter=${this.charCounter}
.endAligned=${this.endAligned}
.prefix=${this.prefix}
.name=${this.name}
.inputMode=${this.inputMode}
.readOnly=${this.readOnly}
.autocapitalize=${this.autocapitalize}
.type=${this._unmaskedPassword ? "text" : "password"}
.suffix=${html`<div style="width: 24px"></div>`}
@input=${this._handleInputChange}
></ha-textfield>
<ha-icon-button
toggles
.label=${this.hass?.localize(
this._unmaskedPassword
? "ui.components.selectors.text.hide_password"
: "ui.components.selectors.text.show_password"
) || (this._unmaskedPassword ? "Hide password" : "Show password")}
@click=${this._toggleUnmaskedPassword}
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-icon-button>`;
}
public checkValidity(): boolean {
return this._textField.checkValidity();
}
public reportValidity(): boolean {
return this._textField.reportValidity();
}
public setCustomValidity(message: string): void {
return this._textField.setCustomValidity(message);
}
public layout(): Promise<void> {
return this._textField.layout();
}
private _toggleUnmaskedPassword(): void {
this._unmaskedPassword = !this._unmaskedPassword;
}
@eventOptions({ passive: true })
private _handleInputChange(ev) {
this.value = ev.target.value;
}
static styles = css`
:host {
display: block;
position: relative;
}
ha-textfield {
width: 100%;
}
ha-icon-button {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-password-field": HaPasswordField;
}
}
+31 -11
View File
@@ -499,8 +499,23 @@ export class HaServiceControl extends LitElement {
.defaultValue=${this._value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: serviceData?.fields.map((dataField) =>
dataField.fields
: serviceData?.fields.map((dataField) => {
if (!dataField.fields) {
return this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
);
}
const fields = Object.entries(dataField.fields).map(
([key, field]) => ({ key, ...field })
);
return fields.length &&
this._hasFilteredFields(fields, targetEntities)
? html`<ha-expansion-panel
leftChevron
.expanded=${!dataField.collapsed}
@@ -531,14 +546,8 @@ export class HaServiceControl extends LitElement {
)
)}
</ha-expansion-panel>`
: this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
)
)} `;
: nothing;
})} `;
}
private _getSectionDescription(
@@ -551,6 +560,16 @@ export class HaServiceControl extends LitElement {
);
}
private _hasFilteredFields(
dataFields: ExtHassService["fields"],
targetEntities: string[]
) {
return dataFields.some(
(dataField) =>
!dataField.filter || this._filterField(dataField.filter, targetEntities)
);
}
private _renderField = (
dataField: ExtHassService["fields"][number],
hasOptional: boolean,
@@ -805,7 +824,8 @@ export class HaServiceControl extends LitElement {
const value = ev.detail.value;
if (
this._value?.data?.[key] === value ||
(!this._value?.data?.[key] && (value === "" || value === undefined))
((!this._value?.data || !(key in this._value.data)) &&
(value === "" || value === undefined))
) {
return;
}
+15 -5
View File
@@ -6,7 +6,7 @@ import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-textfield")
export class HaTextField extends TextFieldBase {
@property({ type: Boolean }) public invalid = false;
@property({ type: Boolean }) public invalid?: boolean;
@property({ attribute: "error-message" }) public errorMessage?: string;
@@ -28,14 +28,24 @@ export class HaTextField extends TextFieldBase {
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (
(changedProperties.has("invalid") &&
(this.invalid || changedProperties.get("invalid") !== undefined)) ||
changedProperties.has("invalid") ||
changedProperties.has("errorMessage")
) {
this.setCustomValidity(
this.invalid ? this.errorMessage || "Invalid" : ""
this.invalid
? this.errorMessage || this.validationMessage || "Invalid"
: ""
);
this.reportValidity();
if (
this.invalid ||
this.validateOnInitialRender ||
(changedProperties.has("invalid") &&
changedProperties.get("invalid") !== undefined)
) {
// Only report validity if the field is invalid or the invalid state has changed from
// true to false to prevent setting empty required fields to invalid on first render
this.reportValidity();
}
}
if (changedProperties.has("autocomplete")) {
if (this.autocomplete) {
+53 -34
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import {
css,
CSSResultGroup,
@@ -6,11 +7,14 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera";
import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc";
import {
fetchWebRtcClientConfiguration,
handleWebRtcOffer,
WebRtcAnswer,
} from "../data/camera";
import type { HomeAssistant } from "../types";
import "./ha-alert";
@@ -37,12 +41,11 @@ class HaWebRtcPlayer extends LitElement {
@property({ type: Boolean, attribute: "playsinline" })
public playsInline = false;
@property() public posterUrl!: string;
@property({ attribute: "poster-url" }) public posterUrl?: string;
@state() private _error?: string;
// don't cache this, as we remove it on disconnects
@query("#remote-stream") private _videoEl!: HTMLVideoElement;
@query("#remote-stream", true) private _videoEl!: HTMLVideoElement;
private _peerConnection?: RTCPeerConnection;
@@ -59,7 +62,7 @@ class HaWebRtcPlayer extends LitElement {
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
.poster=${this.posterUrl}
poster=${ifDefined(this.posterUrl)}
@loadeddata=${this._loadedData}
></video>
`;
@@ -81,20 +84,30 @@ class HaWebRtcPlayer extends LitElement {
if (!changedProperties.has("entityid")) {
return;
}
if (!this._videoEl) {
return;
}
this._startWebRtc();
}
private async _startWebRtc(): Promise<void> {
console.time("WebRTC");
this._error = undefined;
const configuration = await this._fetchPeerConfiguration();
const peerConnection = new RTCPeerConnection(configuration);
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
peerConnection.createDataChannel("dataSendChannel");
console.timeLog("WebRTC", "start clientConfig");
const clientConfig = await fetchWebRtcClientConfiguration(
this.hass,
this.entityid
);
console.timeLog("WebRTC", "end clientConfig", clientConfig);
const peerConnection = new RTCPeerConnection(clientConfig.configuration);
if (clientConfig.dataChannel) {
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
peerConnection.createDataChannel(clientConfig.dataChannel);
}
peerConnection.addTransceiver("audio", { direction: "recvonly" });
peerConnection.addTransceiver("video", { direction: "recvonly" });
@@ -102,30 +115,48 @@ class HaWebRtcPlayer extends LitElement {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
console.timeLog("WebRTC", "start createOffer", offerOptions);
const offer: RTCSessionDescriptionInit =
await peerConnection.createOffer(offerOptions);
console.timeLog("WebRTC", "end createOffer", offer);
console.timeLog("WebRTC", "start setLocalDescription");
await peerConnection.setLocalDescription(offer);
console.timeLog("WebRTC", "end setLocalDescription");
console.timeLog("WebRTC", "start iceResolver");
let candidates = ""; // Build an Offer SDP string with ice candidates
const iceResolver = new Promise<void>((resolve) => {
peerConnection.addEventListener("icecandidate", async (event) => {
peerConnection.addEventListener("icecandidate", (event) => {
if (!event.candidate?.candidate) {
resolve(); // Gathering complete
return;
}
console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
candidates += `a=${event.candidate.candidate}\r\n`;
});
});
await iceResolver;
console.timeLog("WebRTC", "end iceResolver", candidates);
const offer_sdp = offer.sdp! + candidates;
let webRtcAnswer: WebRtcAnswer;
try {
console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
webRtcAnswer = await handleWebRtcOffer(
this.hass,
this.entityid,
offer_sdp
);
console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
} catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message;
peerConnection.close();
@@ -135,6 +166,7 @@ class HaWebRtcPlayer extends LitElement {
// Setup callbacks to render remote stream once media tracks are discovered.
const remoteStream = new MediaStream();
peerConnection.addEventListener("track", (event) => {
console.timeLog("WebRTC", "track", event);
remoteStream.addTrack(event.track);
this._videoEl.srcObject = remoteStream;
});
@@ -146,7 +178,9 @@ class HaWebRtcPlayer extends LitElement {
sdp: webRtcAnswer.answer,
});
try {
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
await peerConnection.setRemoteDescription(remoteDesc);
console.timeLog("WebRTC", "end setRemoteDescription");
} catch (err: any) {
this._error = "Failed to connect WebRTC stream: " + err.message;
peerConnection.close();
@@ -155,23 +189,6 @@ class HaWebRtcPlayer extends LitElement {
this._peerConnection = peerConnection;
}
private async _fetchPeerConfiguration(): Promise<RTCConfiguration> {
if (!isComponentLoaded(this.hass!, "rtsp_to_webrtc")) {
return {};
}
const settings = await fetchWebRtcSettings(this.hass!);
if (!settings || !settings.stun_server) {
return {};
}
return {
iceServers: [
{
urls: [`stun:${settings.stun_server!}`],
},
],
};
}
private _cleanUp() {
if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => {
@@ -190,6 +207,8 @@ class HaWebRtcPlayer extends LitElement {
}
private _loadedData() {
console.timeLog("WebRTC", "loadedData");
console.timeEnd("WebRTC");
// @ts-ignore
fireEvent(this, "load");
}
+9 -10
View File
@@ -18,7 +18,7 @@ import type { HaCodeEditor } from "./ha-code-editor";
import "./ha-button";
const isEmpty = (obj: Record<string, unknown>): boolean => {
if (typeof obj !== "object") {
if (typeof obj !== "object" || obj === null) {
return false;
}
for (const key in obj) {
@@ -59,14 +59,13 @@ export class HaYamlEditor extends LitElement {
public setValue(value): void {
try {
this._yaml =
value && !isEmpty(value)
? dump(value, {
schema: this.yamlSchema,
quotingType: '"',
noRefs: true,
})
: "";
this._yaml = !isEmpty(value)
? dump(value, {
schema: this.yamlSchema,
quotingType: '"',
noRefs: true,
})
: "";
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err, value);
@@ -75,7 +74,7 @@ export class HaYamlEditor extends LitElement {
}
protected firstUpdated(): void {
if (this.defaultValue) {
if (this.defaultValue !== undefined) {
this.setValue(this.defaultValue);
}
}
+1 -1
View File
@@ -94,7 +94,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(config, path)}
?active=${this.selected === path}
.iconPath=${mdiAsterisk}
.notEnabled=${config.enabled === false}
.notEnabled=${"enabled" in config && config.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${track ? "0" : "-1"}
></hat-graph-node>
+2 -21
View File
@@ -22,13 +22,8 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { fullEntitiesContext, labelsContext } from "../../data/context";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FloorRegistryEntry } from "../../data/floor_registry";
import { LabelRegistryEntry } from "../../data/label_registry";
import { LogbookEntry } from "../../data/logbook";
import {
@@ -206,7 +201,6 @@ class ActionRenderer {
private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[],
private labelReg: LabelRegistryEntry[],
private floorReg: FloorRegistryEntry[],
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer,
@@ -325,7 +319,6 @@ class ActionRenderer {
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
data,
actionType
),
@@ -493,13 +486,7 @@ class ActionRenderer {
const name =
repeatConfig.alias ||
describeAction(
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
repeatConfig
);
describeAction(this.hass, this.entityReg, this.labelReg, repeatConfig);
this._renderEntry(repeatPath, name, undefined, disabled);
@@ -597,7 +584,6 @@ class ActionRenderer {
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
sequenceConfig,
"sequence"
),
@@ -694,10 +680,6 @@ export class HaAutomationTracer extends LitElement {
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: FloorRegistryEntry[];
protected render() {
if (!this.trace) {
return nothing;
@@ -715,7 +697,6 @@ export class HaAutomationTracer extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
entries,
this.trace,
logbookRenderer,
+16 -6
View File
@@ -8,6 +8,7 @@ import { Context, HomeAssistant } from "../types";
import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action, MODES, migrateAutomationAction } from "./script";
import { createSearchParam } from "../common/url/search-params";
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10;
@@ -166,7 +167,7 @@ export interface TagTrigger extends BaseTrigger {
export interface TimeTrigger extends BaseTrigger {
trigger: "time";
at: string;
at: string | { entity_id: string; offset?: string };
}
export interface TemplateTrigger extends BaseTrigger {
@@ -206,7 +207,8 @@ export type Trigger =
| TemplateTrigger
| EventTrigger
| DeviceTrigger
| CalendarTrigger;
| CalendarTrigger
| TriggerList;
interface BaseCondition {
condition: string;
@@ -426,6 +428,10 @@ export const migrateAutomationTrigger = (
return trigger.map(migrateAutomationTrigger) as Trigger[];
}
if ("triggers" in trigger && trigger.triggers) {
trigger.triggers = migrateAutomationTrigger(trigger.triggers);
}
if ("platform" in trigger) {
if (!("trigger" in trigger)) {
// @ts-ignore
@@ -437,7 +443,7 @@ export const migrateAutomationTrigger = (
};
export const flattenTriggers = (
triggers: undefined | Trigger | (Trigger | TriggerList)[]
triggers: undefined | Trigger | Trigger[]
): Trigger[] => {
if (!triggers) {
return [];
@@ -448,7 +454,7 @@ export const flattenTriggers = (
ensureArray(triggers).forEach((t) => {
if ("triggers" in t) {
if (t.triggers) {
flatTriggers.push(...ensureArray(t.triggers));
flatTriggers.push(...flattenTriggers(t.triggers));
}
} else {
flatTriggers.push(t);
@@ -457,9 +463,13 @@ export const flattenTriggers = (
return flatTriggers;
};
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
export const showAutomationEditor = (
data?: Partial<AutomationConfig>,
expanded?: boolean
) => {
initialAutomationEditorData = data;
navigate("/config/automation/edit/new");
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : "";
navigate(`/config/automation/edit/new${params}`);
};
export const duplicateAutomation = (config: AutomationConfig) => {
+32 -7
View File
@@ -8,6 +8,7 @@ import {
import secondsToDuration from "../common/datetime/seconds_to_duration";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import { computeStateName } from "../common/entity/compute_state_name";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import type { HomeAssistant } from "../types";
import { Condition, ForDict, Trigger } from "./automation";
import {
@@ -22,6 +23,7 @@ import {
formatListWithAnds,
formatListWithOrs,
} from "../common/string/format-list";
import { isTriggerList } from "./trigger";
const triggerTranslationBaseKey =
"ui.panel.config.automation.editor.triggers.type";
@@ -98,6 +100,20 @@ const tryDescribeTrigger = (
entityRegistry: EntityRegistryEntry[],
ignoreAlias = false
) => {
if (isTriggerList(trigger)) {
const triggers = ensureArray(trigger.triggers);
if (!triggers || triggers.length === 0) {
return hass.localize(
`${triggerTranslationBaseKey}.list.description.no_trigger`
);
}
const count = triggers.length;
return hass.localize(`${triggerTranslationBaseKey}.list.description.full`, {
count: count,
});
}
if (trigger.alias && !ignoreAlias) {
return trigger.alias;
}
@@ -356,13 +372,22 @@ const tryDescribeTrigger = (
// Time Trigger
if (trigger.trigger === "time" && trigger.at) {
const result = ensureArray(trigger.at).map((at) =>
typeof at !== "string"
? at
: at.includes(".")
? `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`
: localizeTimeString(at, hass.locale, hass.config)
);
const result = ensureArray(trigger.at).map((at) => {
if (typeof at === "string") {
if (isValidEntityId(at)) {
return `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`;
}
return localizeTimeString(at, hass.locale, hass.config);
}
const entityStr = `entity ${hass.states[at.entity_id] ? computeStateName(hass.states[at.entity_id]) : at.entity_id}`;
const offsetStr = at.offset
? " " +
hass.localize(`${triggerTranslationBaseKey}.time.offset_by`, {
offset: describeDuration(hass.locale, at.offset),
})
: "";
return `${entityStr}${offsetStr}`;
});
return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, {
time: formatListWithOrs(hass.locale, result),
+14
View File
@@ -133,3 +133,17 @@ export const isCameraMediaSource = (mediaContentId: string) =>
export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length);
export interface WebRTCClientConfiguration {
configuration: RTCConfiguration;
dataChannel?: string;
}
export const fetchWebRtcClientConfiguration = async (
hass: HomeAssistant,
entityId: string
) =>
hass.callWS<WebRTCClientConfiguration>({
type: "camera/webrtc/get_client_config",
entity_id: entityId,
});
-3
View File
@@ -2,7 +2,6 @@ import { createContext } from "@lit-labs/context";
import { HassConfig } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { EntityRegistryEntry } from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { LabelRegistryEntry } from "./label_registry";
export const connectionContext =
@@ -28,6 +27,4 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
export const fullEntitiesContext =
createContext<EntityRegistryEntry[]>("extendedEntities");
export const floorsContext = createContext<FloorRegistryEntry[]>("floors");
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
+1
View File
@@ -28,6 +28,7 @@ export interface UrlActionConfig extends BaseActionConfig {
export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info";
entity_id?: string;
}
export interface AssistActionConfig extends BaseActionConfig {
+4
View File
@@ -17,6 +17,10 @@ export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
cards?: LovelaceCardConfig[];
}
export interface LovelaceGridSectionConfig extends LovelaceSectionConfig {
grid_base?: number;
}
export interface LovelaceStrategySectionConfig
extends LovelaceBaseSectionConfig {
strategy: LovelaceStrategyConfig;
+8 -5
View File
@@ -50,7 +50,7 @@ export interface StatisticsMetaData {
export const STATISTIC_TYPES: StatisticsValidationResult["type"][] = [
"entity_not_recorded",
"entity_no_longer_recorded",
"unsupported_state_class",
"state_class_removed",
"units_changed",
"no_state",
];
@@ -59,7 +59,7 @@ export type StatisticsValidationResult =
| StatisticsValidationResultNoState
| StatisticsValidationResultEntityNotRecorded
| StatisticsValidationResultEntityNoLongerRecorded
| StatisticsValidationResultUnsupportedStateClass
| StatisticsValidationResultStateClassRemoved
| StatisticsValidationResultUnitsChanged;
export interface StatisticsValidationResultNoState {
@@ -77,9 +77,9 @@ export interface StatisticsValidationResultEntityNotRecorded {
data: { statistic_id: string };
}
export interface StatisticsValidationResultUnsupportedStateClass {
type: "unsupported_state_class";
data: { statistic_id: string; state_class: string };
export interface StatisticsValidationResultStateClassRemoved {
type: "state_class_removed";
data: { statistic_id: string };
}
export interface StatisticsValidationResultUnitsChanged {
@@ -332,3 +332,6 @@ export const getDisplayUnit = (
export const isExternalStatistic = (statisticsId: string): boolean =>
statisticsId.includes(":");
export const updateStatisticsIssues = (hass: HomeAssistant) =>
hass.callWS({ type: "recorder/update_statistics_issues" });
-10
View File
@@ -1,10 +0,0 @@
import { HomeAssistant } from "../types";
export interface WebRtcSettings {
stun_server?: string;
}
export const fetchWebRtcSettings = async (hass: HomeAssistant) =>
hass.callWS<WebRtcSettings>({
type: "rtsp_to_webrtc/get_settings",
});
+7 -2
View File
@@ -28,6 +28,7 @@ import {
} from "./automation";
import { BlueprintInput } from "./blueprint";
import { computeObjectId } from "../common/entity/compute_object_id";
import { createSearchParam } from "../common/url/search-params";
export const MODES = ["single", "restart", "queued", "parallel"] as const;
export const MODES_MAX = ["queued", "parallel"] as const;
@@ -347,9 +348,13 @@ export const getScriptStateConfig = (hass: HomeAssistant, entity_id: string) =>
entity_id,
});
export const showScriptEditor = (data?: Partial<ScriptConfig>) => {
export const showScriptEditor = (
data?: Partial<ScriptConfig>,
expanded?: boolean
) => {
inititialScriptEditorData = data;
navigate("/config/script/edit/new");
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : "";
navigate(`/config/script/edit/new${params}`);
};
export const getScriptEditorInitData = () => {
+1 -7
View File
@@ -14,7 +14,6 @@ import {
computeEntityRegistryName,
entityRegistryById,
} from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { domainToName } from "./integration";
import { LabelRegistryEntry } from "./label_registry";
import {
@@ -44,7 +43,6 @@ export const describeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -54,7 +52,6 @@ export const describeAction = <T extends ActionType>(
hass,
entityRegistry,
labelRegistry,
floorRegistry,
action,
actionType,
ignoreAlias
@@ -78,7 +75,6 @@ const tryDescribeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -168,9 +164,7 @@ const tryDescribeAction = <T extends ActionType>(
);
}
} else if (key === "floor_id") {
const floor = floorRegistry.find(
(flr) => flr.floor_id === targetThing
);
const floor = hass.floors[targetThing] ?? undefined;
if (floor?.name) {
targets.push(floor.name);
} else {
+1 -1
View File
@@ -18,7 +18,7 @@ export interface ThreadDataSet {
channel: number | null;
created: string;
dataset_id: string;
extended_pan_id: string | null;
extended_pan_id: string;
network_name: string;
pan_id: string | null;
preferred_border_agent_id: string | null;
+2 -2
View File
@@ -72,8 +72,8 @@ export const timerTimeRemaining = (
if (stateObj.state === "active") {
const now = new Date().getTime();
const madeActive = new Date(stateObj.last_changed).getTime();
timeRemaining = Math.max(timeRemaining - (now - madeActive) / 1000, 0);
const finishes = new Date(stateObj.attributes.finishes_at).getTime();
timeRemaining = Math.max((finishes - now) / 1000, 0);
}
return timeRemaining;
+6 -1
View File
@@ -5,6 +5,7 @@ import {
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiMapClock,
mdiMapMarker,
@@ -21,7 +22,7 @@ import {
} from "@mdi/js";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import { AutomationElementGroup } from "./automation";
import { AutomationElementGroup, Trigger, TriggerList } from "./automation";
export const TRIGGER_ICONS = {
calendar: mdiCalendar,
@@ -41,6 +42,7 @@ export const TRIGGER_ICONS = {
webhook: mdiWebhook,
persistent_notification: mdiMessageAlert,
zone: mdiMapMarkerRadius,
list: mdiFormatListBulleted,
};
export const TRIGGER_GROUPS: AutomationElementGroup = {
@@ -65,3 +67,6 @@ export const TRIGGER_GROUPS: AutomationElementGroup = {
},
},
} as const;
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
"triggers" in trigger;
@@ -9,23 +9,15 @@ import {
html,
nothing,
} from "lit";
import { customElement, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import "../../components/ha-dialog";
import "../../components/ha-icon-button";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import {
DataEntryFlowStep,
subscribeDataEntryFlowProgressed,
} from "../../data/data_entry_flow";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
@@ -62,7 +54,7 @@ declare global {
@customElement("dialog-data-entry-flow")
class DataEntryFlowDialog extends LitElement {
public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: DataEntryFlowDialogParams;
@@ -76,16 +68,8 @@ class DataEntryFlowDialog extends LitElement {
// Null means we need to pick a config flow
| null;
@state() private _devices?: DeviceRegistryEntry[];
@state() private _areas?: AreaRegistryEntry[];
@state() private _handler?: string;
private _unsubAreas?: UnsubscribeFunc;
private _unsubDevices?: UnsubscribeFunc;
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
@@ -183,16 +167,7 @@ class DataEntryFlowDialog extends LitElement {
this._loading = undefined;
this._step = undefined;
this._params = undefined;
this._devices = undefined;
this._handler = undefined;
if (this._unsubAreas) {
this._unsubAreas();
this._unsubAreas = undefined;
}
if (this._unsubDevices) {
this._unsubDevices();
this._unsubDevices = undefined;
}
if (this._unsubDataEntryFlowProgressed) {
this._unsubDataEntryFlowProgressed.then((unsub) => {
unsub();
@@ -309,25 +284,13 @@ class DataEntryFlowDialog extends LitElement {
.hass=${this.hass}
></step-flow-menu>
`
: this._devices === undefined ||
this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry
html`
<step-flow-loading
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
loadingReason="loading_devices_areas"
></step-flow-loading>
`
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.devices=${this._devices}
.areas=${this._areas}
></step-flow-create-entry>
`}
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-create-entry>
`}
`}
</div>
</ha-dialog>
@@ -351,32 +314,6 @@ class DataEntryFlowDialog extends LitElement {
// external and progress step will send update event from the backend, so we should subscribe to them
this._subscribeDataEntryFlowProgressed();
}
if (this._step.type === "create_entry") {
if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) {
this._fetchDevices(this._step.result.entry_id);
this._fetchAreas();
} else {
this._devices = [];
this._areas = [];
}
}
}
private async _fetchDevices(configEntryId) {
this._unsubDevices = subscribeDeviceRegistry(
this.hass.connection,
(devices) => {
this._devices = devices.filter((device) =>
device.config_entries.includes(configEntryId)
);
}
);
}
private async _fetchAreas() {
this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
});
}
private async _processStep(
@@ -20,7 +20,7 @@ export const showConfigFlowDialog = (
): void =>
showFlowDialog(element, dialogParams, {
flowType: "config_flow",
loadDevicesAndAreas: true,
showDevices: true,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createConfigFlow(hass, handler, dialogParams.entryId),
@@ -17,7 +17,7 @@ import type { HomeAssistant } from "../../types";
export interface FlowConfig {
flowType: FlowType;
loadDevicesAndAreas: boolean;
showDevices: boolean;
createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>;
@@ -134,8 +134,7 @@ export interface FlowConfig {
export type LoadingReason =
| "loading_handlers"
| "loading_flow"
| "loading_step"
| "loading_devices_areas";
| "loading_step";
export interface DataEntryFlowDialogParams {
startFlowHandler?: string;
@@ -29,7 +29,7 @@ export const showOptionsFlowDialog = (
},
{
flowType: "options_flow",
loadDevicesAndAreas: false,
showDevices: false,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createOptionsFlow(hass, handler),
@@ -4,6 +4,7 @@ import {
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
} from "lit";
@@ -34,7 +35,16 @@ class StepFlowCreateEntry extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
@property({ attribute: false }) public devices!: DeviceRegistryEntry[];
private _devices = memoizeOne(
(
showDevices: boolean,
devices: DeviceRegistryEntry[],
entry_id?: string
) =>
showDevices && entry_id
? devices.filter((device) => device.config_entries.includes(entry_id))
: []
);
private _deviceEntities = memoizeOne(
(
@@ -50,35 +60,48 @@ class StepFlowCreateEntry extends LitElement {
);
protected willUpdate(changedProps: PropertyValues) {
if (!changedProps.has("devices") && !changedProps.has("hass")) {
return;
}
const devices = this._devices(
this.flowConfig.showDevices,
Object.values(this.hass.devices),
this.step.result?.entry_id
);
if (
(changedProps.has("devices") || changedProps.has("hass")) &&
this.devices.length === 1
devices.length !== 1 ||
devices[0].primary_config_entry !== this.step.result?.entry_id
) {
// integration_type === "device"
const assistSatellites = this._deviceEntities(
this.devices[0].id,
Object.values(this.hass.entities),
"assist_satellite"
);
if (
assistSatellites.length &&
assistSatellites.some((satellite) =>
assistSatelliteSupportsSetupFlow(
this.hass.states[satellite.entity_id]
)
)
) {
this._flowDone();
showVoiceAssistantSetupDialog(this, {
deviceId: this.devices[0].id,
});
}
return;
}
const assistSatellites = this._deviceEntities(
devices[0].id,
Object.values(this.hass.entities),
"assist_satellite"
);
if (
assistSatellites.length &&
assistSatellites.some((satellite) =>
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
)
) {
this._flowDone();
showVoiceAssistantSetupDialog(this, {
deviceId: devices[0].id,
});
}
}
protected render(): TemplateResult {
const localize = this.hass.localize;
const devices = this._devices(
this.flowConfig.showDevices,
Object.values(this.hass.devices),
this.step.result?.entry_id
);
return html`
<h2>${localize("ui.panel.config.integrations.config_flow.success")}!</h2>
<div class="content">
@@ -89,9 +112,9 @@ class StepFlowCreateEntry extends LitElement {
"ui.panel.config.integrations.config_flow.not_loaded"
)}</span
>`
: ""}
${this.devices.length === 0
? ""
: nothing}
${devices.length === 0
? nothing
: html`
<p>
${localize(
@@ -99,7 +122,7 @@ class StepFlowCreateEntry extends LitElement {
)}:
</p>
<div class="devices">
${this.devices.map(
${devices.map(
(device) => html`
<div class="device">
<div>
@@ -18,6 +18,7 @@ import {
updateReleaseNotes,
} from "../../../data/update";
import type { HomeAssistant } from "../../../types";
import { showAlertDialog } from "../../generic/show-dialog-box";
@customElement("more-info-update")
class MoreInfoUpdate extends LitElement {
@@ -127,29 +128,27 @@ class MoreInfoUpdate extends LitElement {
</ha-formfield> `
: ""}
<div class="actions">
${this.stateObj.attributes.auto_update
? ""
: this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
? html`
<mwc-button @click=${this._handleClearSkipped}>
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</mwc-button>
`
: html`
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</mwc-button>
`}
${this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
? html`
<mwc-button @click=${this._handleClearSkipped}>
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</mwc-button>
`
: html`
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</mwc-button>
`}
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
? html`
<mwc-button
@@ -211,6 +210,17 @@ class MoreInfoUpdate extends LitElement {
}
private _handleSkip(): void {
if (this.stateObj!.attributes.auto_update) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.auto_update_enabled_title"
),
text: this.hass.localize(
"ui.dialogs.more_info_control.update.auto_update_enabled_text"
),
});
return;
}
this.hass.callService("update", "skip", {
entity_id: this.stateObj!.entity_id,
});
@@ -51,6 +51,7 @@ export class HuiPersistentNotificationItem extends LitElement {
static get styles(): CSSResultGroup {
return css`
.time {
position: relative;
display: flex;
justify-content: flex-end;
margin-top: 6px;
+15 -5
View File
@@ -8,7 +8,6 @@ export const AssistantSetupStyles = [
align-items: center;
text-align: center;
min-height: 300px;
max-width: 500px;
display: flex;
flex-direction: column;
justify-content: space-between;
@@ -21,16 +20,27 @@ export const AssistantSetupStyles = [
}
.content img {
width: 120px;
margin-top: 68px;
margin-bottom: 68px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.content img {
margin-top: 68px;
margin-bottom: 68px;
}
}
.footer {
width: 100%;
display: flex;
width: 100%;
flex-direction: row;
justify-content: flex-end;
}
.footer.full-width {
flex-direction: column;
}
.footer ha-button {
.footer.full-width ha-button {
width: 100%;
}
.footer.side-by-side {
justify-content: space-between;
}
`,
];
@@ -1,5 +1,5 @@
import "@material/mwc-button/mwc-button";
import { mdiChevronLeft } from "@mdi/js";
import { mdiChevronLeft, mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -50,6 +50,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
private _previousSteps: STEP[] = [];
private _nextStep?: STEP;
public async showDialog(
params: VoiceAssistantSetupDialogParams
): Promise<void> {
@@ -113,19 +115,38 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
@closed=${this._dialogClosed}
.heading=${"Voice Satellite setup"}
hideActions
escapeKeyAction
scrimClickAction
>
<ha-dialog-header slot="heading">
${this._previousSteps.length
? html`<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.dialogs.generic.close") ??
"Close"}
.label=${this.hass.localize("ui.common.back") ?? "Back"}
.path=${mdiChevronLeft}
@click=${this._goToPreviousStep}
></ha-icon-button>`
: this._step !== STEP.UPDATE
? html`<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.dialogs.generic.close") ??
"Close"}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>`
: nothing}
${this._step === STEP.WAKEWORD ||
this._step === STEP.AREA ||
this._step === STEP.PIPELINE
? html`<ha-button
@click=${this._goToNextStep}
class="skip-btn"
slot="actionItems"
>Skip</ha-button
>`
: nothing}
</ha-dialog-header>
<div class="content" @next-step=${this._nextStep}>
<div class="content" @next-step=${this._goToNextStep}>
${this._step === STEP.UPDATE
? html`<ha-voice-assistant-setup-step-update
.hass=${this.hass}
@@ -229,15 +250,21 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
this._step = this._previousSteps.pop()!;
}
private _nextStep(ev) {
private _goToNextStep(ev) {
if (ev.detail?.updateConfig) {
this._fetchAssistConfiguration();
}
if (ev.detail?.nextStep) {
this._nextStep = ev.detail.nextStep;
}
if (!ev.detail?.noPrevious) {
this._previousSteps.push(this._step);
}
if (ev.detail?.step) {
this._step = ev.detail.step;
} else if (this._nextStep) {
this._step = this._nextStep;
this._nextStep = undefined;
} else {
this._step += 1;
}
@@ -250,6 +277,14 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
ha-dialog {
--dialog-content-padding: 0;
}
@media all and (min-width: 450px) and (min-height: 500px) {
ha-dialog {
--mdc-dialog-min-width: 560px;
--mdc-dialog-max-width: 560px;
--mdc-dialog-min-width: min(560px, 95vw);
--mdc-dialog-max-width: min(560px, 95vw);
}
}
ha-dialog-header {
height: 56px;
}
@@ -258,6 +293,9 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
height: calc(100vh - 56px);
}
}
.skip-btn {
margin-top: 6px;
}
`,
];
}
@@ -270,7 +308,12 @@ declare global {
interface HASSDomEvents {
"next-step":
| { step?: STEP; updateConfig?: boolean; noPrevious?: boolean }
| {
step?: STEP;
updateConfig?: boolean;
noPrevious?: boolean;
nextStep?: STEP;
}
| undefined;
}
}
@@ -42,24 +42,7 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
powerful device to run. If you device is not powerful enough, Home
Assistant cloud might be a better option.
</p>
<h3>Home Assistant Cloud:</h3>
<div class="messages-container cloud">
<div class="message user ${this._showFirst ? "show" : ""}">
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
</div>
${this._showFirst
? html`<div class="timing user">0.2 seconds</div>`
: nothing}
${this._showFirst
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
${!this._showSecond ? "…" : "Turned on the lights"}
</div>`
: nothing}
${this._showSecond
? html`<div class="timing hass">0.4 seconds</div>`
: nothing}
</div>
<h3>Raspberry Pi 4:</h3>
<h3>Raspberry Pi 4</h3>
<div class="messages-container rpi">
<div class="message user ${this._showThird ? "show" : ""}">
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
@@ -76,8 +59,28 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
? html`<div class="timing hass">5 seconds</div>`
: nothing}
</div>
<h3>Home Assistant Cloud</h3>
<div class="messages-container cloud">
<div class="message user ${this._showFirst ? "show" : ""}">
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
</div>
${this._showFirst
? html`<div class="timing user">0.2 seconds</div>`
: nothing}
${this._showFirst
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
${!this._showSecond ? "…" : "Turned on the lights"}
</div>`
: nothing}
${this._showSecond
? html`<div class="timing hass">0.4 seconds</div>`
: nothing}
</div>
</div>
<div class="footer">
<div class="footer side-by-side">
<ha-button @click=${this._goToCloud}
>Try Home Assistant Cloud</ha-button
>
<a
href=${documentationUrl(
this.hass,
@@ -85,19 +88,14 @@ export class HaVoiceAssistantSetupStepAddons extends LitElement {
)}
target="_blank"
rel="noreferrer noopenner"
@click=${this._close}
><ha-button unelevated
>Learn how to setup local assistant</ha-button
></a
>
<ha-button @click=${this._skip}
>I already have a local assistant</ha-button
>
<ha-button @click=${this._skip} unelevated>Learn more</ha-button>
</a>
</div>`;
}
private _close() {
fireEvent(this, "closed");
private _goToCloud() {
fireEvent(this, "next-step", { step: STEP.CLOUD });
}
private _skip() {
@@ -28,7 +28,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
></ha-area-picker>
</div>
<div class="footer">
<ha-button @click=${this._setArea}>Next</ha-button>
<ha-button @click=${this._setArea} unelevated>Next</ha-button>
</div>`;
}
@@ -25,8 +25,8 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
<img src="/static/icons/casita/smiling.png" />
<h1>Change wake word</h1>
<p class="secondary">
When you voice assistant knows where it is, it can better control the
devices around it.
Some wake words are better for [your language] and voice than others.
Please try them out.
</p>
</div>
<ha-md-list>
@@ -72,6 +72,7 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
ha-md-list {
width: 100%;
text-align: initial;
margin-bottom: 24px;
}
`,
];
@@ -22,7 +22,7 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
if (
this._status === "success" &&
changedProperties.has("hass") &&
this.hass.states[this.assistEntityId!]?.state === "listening_wake_word"
this.hass.states[this.assistEntityId!]?.state === "idle"
) {
this._nextStep();
}
@@ -38,16 +38,13 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
</p>`
: this._status === "timeout"
? html`<img src="/static/icons/casita/sad.png" />
<h1>Error</h1>
<h1>Voice assistant can not connect to Home Assistant</h1>
<p class="secondary">
Your device was unable to reach Home Assistant. Make sure you
have setup your
<a href="/config/network" @click=${this._close}
>Home Assistant URL's</a
>
correctly.
A good explanation what is happening and what action you should
take.
</p>
<div class="footer">
<a href="#"><ha-button>Help me</ha-button></a>
<ha-button @click=${this._testConnection}>Retry</ha-button>
</div>`
: html`<img src="/static/icons/casita/loading.png" />
@@ -73,10 +70,6 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
fireEvent(this, "next-step", { noPrevious: true });
}
private _close() {
fireEvent(this, "closed");
}
static styles = AssistantSetupStyles;
}
@@ -10,16 +10,24 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
protected override render() {
return html`<div class="content">
<img src="/static/icons/casita/loving.png" />
<h1>Home Assistant Cloud</h1>
<img src="/static/images/logo_nabu_casa.png" />
<h1>Supercharge your assistant with Home Assistant Cloud</h1>
<p class="secondary">
With Home Assistant Cloud, you get the best results for your voice
assistant, sign up for a free trial now.
Speed up and take the load off your system by running your
text-to-speech and speech-to-text in our private and secure cloud.
Cloud also includes secure remote access to your system while
supporting the development of Home Assistant.
</p>
</div>
<div class="footer">
<div class="footer side-by-side">
<a
href="https://www.nabucasa.com"
target="_blank"
rel="noreferrer noopenner"
><ha-button>Learn more</ha-button></a
>
<a href="/config/cloud/register" @click=${this._close}
><ha-button>Start your free trial</ha-button></a
><ha-button unelevated>Try 1 month for free</ha-button></a
>
</div>`;
}
@@ -92,7 +92,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
)}
rel="noreferrer noopenner"
target="_blank"
@click=${this._close}
@click=${this._skip}
>
Use external system
<span slot="supporting-text"
@@ -144,7 +144,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
{ option: "preferred" },
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
);
this._nextStep(STEP.SUCCESS);
fireEvent(this, "next-step", { step: STEP.SUCCESS, noPrevious: true });
return;
}
}
@@ -210,25 +210,25 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
{ option: cloudPipeline.name },
{ entity_id: this.assistConfiguration?.pipeline_entity_id }
);
this._nextStep(STEP.SUCCESS);
fireEvent(this, "next-step", { step: STEP.SUCCESS, noPrevious: true });
}
private async _setupCloud() {
fireEvent(this, "next-step", { step: STEP.CLOUD });
this._nextStep(STEP.CLOUD);
}
private async _thisSystem() {
fireEvent(this, "next-step", { step: STEP.ADDONS });
this._nextStep(STEP.ADDONS);
}
private _skip() {
this._nextStep(STEP.SUCCESS);
}
private _nextStep(step?: STEP) {
fireEvent(this, "next-step", { step });
}
private _close() {
fireEvent(this, "closed");
}
static styles = [
AssistantSetupStyles,
css`
@@ -1,9 +1,9 @@
import { mdiCog, mdiMicrophone, mdiPlay } from "@mdi/js";
import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import "../../components/ha-md-list-item";
import "../../components/ha-select";
import "../../components/ha-tts-voice-picker";
import {
AssistPipeline,
@@ -14,6 +14,7 @@ import {
import {
assistSatelliteAnnounce,
AssistSatelliteConfiguration,
setWakeWords,
} from "../../data/assist_satellite";
import { fetchCloudStatus } from "../../data/cloud";
import { showVoiceAssistantPipelineDetailDialog } from "../../panels/config/voice-assistants/show-dialog-voice-assistant-pipeline-detail";
@@ -21,6 +22,8 @@ import "../../panels/lovelace/entity-rows/hui-select-entity-row";
import { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
import { STEP } from "./voice-assistant-setup-dialog";
import { setSelectOption } from "../../data/select";
import { InputSelectEntity } from "../../data/input_select";
@customElement("ha-voice-assistant-setup-step-success")
export class HaVoiceAssistantSetupStepSuccess extends LitElement {
@@ -56,58 +59,87 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
}
}
private _activeWakeWord = memoizeOne(
(config: AssistSatelliteConfiguration | undefined) => {
if (!config) {
return "";
}
const activeId = config.active_wake_words[0];
return config.available_wake_words.find((ww) => ww.id === activeId)
?.wake_word;
}
);
protected override render() {
const pipelineEntity = this.assistConfiguration
? (this.hass.states[
this.assistConfiguration.pipeline_entity_id
] as InputSelectEntity)
: undefined;
return html`<div class="content">
<img src="/static/icons/casita/loving.png" />
<h1>Ready to assist!</h1>
<p class="secondary">
Make your assistant more personal by customizing shizzle to the
manizzle
Your device is all ready to go! If you want to tweak some more
settings, you can change that below.
</p>
<ha-md-list-item
interactive
type="button"
@click=${this._changeWakeWord}
>
Change wake word
<span slot="supporting-text"
>${this._activeWakeWord(this.assistConfiguration)}</span
>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<hui-select-entity-row
.hass=${this.hass}
._config=${{
entity: this.assistConfiguration?.pipeline_entity_id,
}}
></hui-select-entity-row>
${this._ttsSettings
? html`<ha-tts-voice-picker
.hass=${this.hass}
required
.engineId=${this._ttsSettings.engine}
.language=${this._ttsSettings.language}
.value=${this._ttsSettings.voice}
@value-changed=${this._voicePicked}
@closed=${stopPropagation}
></ha-tts-voice-picker>`
: nothing}
<div class="rows">
${this.assistConfiguration &&
this.assistConfiguration.available_wake_words.length > 1
? html` <div class="row">
<ha-select
.label=${"Wake word"}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this.assistConfiguration.active_wake_words[0]}
@selected=${this._wakeWordPicked}
>
${this.assistConfiguration.available_wake_words.map(
(wakeword) =>
html`<ha-list-item .value=${wakeword.id}>
${wakeword.wake_word}
</ha-list-item>`
)}
</ha-select>
<ha-button @click=${this._testWakeWord}>
<ha-svg-icon slot="icon" .path=${mdiMicrophone}></ha-svg-icon>
Test
</ha-button>
</div>`
: nothing}
${pipelineEntity
? html`<div class="row">
<ha-select
.label=${"Assistant"}
@closed=${stopPropagation}
.value=${pipelineEntity?.state}
fixedMenuPosition
naturalMenuWidth
@selected=${this._pipelinePicked}
>
${pipelineEntity?.attributes.options.map(
(pipeline) =>
html`<ha-list-item .value=${pipeline}>
${this.hass.formatEntityState(pipelineEntity, pipeline)}
</ha-list-item>`
)}
</ha-select>
<ha-button @click=${this._openPipeline}>
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
Edit
</ha-button>
</div>`
: nothing}
${this._ttsSettings
? html`<div class="row">
<ha-tts-voice-picker
.hass=${this.hass}
.engineId=${this._ttsSettings.engine}
.language=${this._ttsSettings.language}
.value=${this._ttsSettings.voice}
@value-changed=${this._voicePicked}
@closed=${stopPropagation}
></ha-tts-voice-picker>
<ha-button @click=${this._testTts}>
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
Try
</ha-button>
</div>`
: nothing}
</div>
</div>
<div class="footer">
<ha-button @click=${this._openPipeline}
>Change assistant settings</ha-button
>
<ha-button @click=${this._close} unelevated>Done</ha-button>
</div>`;
}
@@ -136,6 +168,25 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
return [pipeline, pipelines.preferred_pipeline];
}
private async _wakeWordPicked(ev) {
const option = ev.target.value;
await setWakeWords(this.hass, this.assistEntityId!, [option]);
}
private _pipelinePicked(ev) {
const stateObj = this.hass!.states[
this.assistConfiguration!.pipeline_entity_id
] as InputSelectEntity;
const option = ev.target.value;
if (
option === stateObj.state ||
!stateObj.attributes.options.includes(option)
) {
return;
}
setSelectOption(this.hass!, stateObj.entity_id, option);
}
private async _setTtsSettings() {
const [pipeline] = await this._getPipeline();
if (!pipeline) {
@@ -160,6 +211,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
...pipeline,
tts_voice: ev.detail.value,
});
}
private _testTts() {
this._announce("Hello, how can I help you?");
}
@@ -170,8 +224,12 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
await assistSatelliteAnnounce(this.hass, this.assistEntityId, message);
}
private _changeWakeWord() {
fireEvent(this, "next-step", { step: STEP.CHANGE_WAKEWORD });
private _testWakeWord() {
fireEvent(this, "next-step", {
step: STEP.WAKEWORD,
nextStep: STEP.SUCCESS,
updateConfig: true,
});
}
private async _openPipeline() {
@@ -209,12 +267,28 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
text-align: initial;
}
ha-tts-voice-picker {
margin-top: 16px;
display: block;
}
.footer {
margin-top: 24px;
}
.rows {
gap: 16px;
display: flex;
flex-direction: column;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
}
.row > *:first-child {
flex: 1;
margin-right: 4px;
}
.row ha-button {
width: 82px;
}
`,
];
}
@@ -2,7 +2,7 @@ import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import { UNAVAILABLE } from "../../data/entity";
import { OFF, ON, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
@@ -14,34 +14,36 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
private _updated = false;
private _refreshTimeout?: number;
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (!this.updateEntityId) {
this._nextStep();
return;
}
if (changedProperties.has("hass") && this.updateEntityId) {
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
if (oldHass) {
const oldState = oldHass.states[this.updateEntityId];
const newState = this.hass.states[this.updateEntityId];
if (
oldState?.state === UNAVAILABLE &&
newState?.state !== UNAVAILABLE
(oldState?.state === UNAVAILABLE &&
newState?.state !== UNAVAILABLE) ||
(oldState?.state !== ON && newState?.state === ON)
) {
// Device is rebooted, let's move on
this._tryUpdate();
this._tryUpdate(false);
return;
}
}
}
if (!changedProperties.has("updateEntityId")) {
return;
if (changedProperties.has("updateEntityId")) {
this._tryUpdate(true);
}
if (!this.updateEntityId) {
this._nextStep();
return;
}
this._tryUpdate();
}
protected override render() {
@@ -56,7 +58,11 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
return html`<div class="content">
<img src="/static/icons/casita/loading.png" />
<h1>Updating your voice assistant</h1>
<h1>
${stateObj.state === OFF || stateObj.state === UNKNOWN
? "Checking for updates"
: "Updating your voice assistant"}
</h1>
<p class="secondary">
We are making sure you have the latest and greatest version of your
voice assistant. This may take a few minutes.
@@ -77,15 +83,13 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
</div>`;
}
private async _tryUpdate() {
private async _tryUpdate(refreshUpdate: boolean) {
clearTimeout(this._refreshTimeout);
if (!this.updateEntityId) {
return;
}
const updateEntity = this.hass.states[this.updateEntityId];
if (
updateEntity &&
this.hass.states[updateEntity.entity_id].state === "on"
) {
if (updateEntity && this.hass.states[updateEntity.entity_id].state === ON) {
this._updated = true;
await this.hass.callService(
"update",
@@ -93,6 +97,16 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
{},
{ entity_id: updateEntity.entity_id }
);
} else if (refreshUpdate) {
await this.hass.callService(
"homeassistant",
"update_entity",
{},
{ entity_id: this.updateEntityId }
);
this._refreshTimeout = window.setTimeout(() => {
this._nextStep();
}, 5000);
} else {
this._nextStep();
}
@@ -58,7 +58,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
const entityState = this.hass.states[this.assistEntityId];
if (entityState.state !== "listening_wake_word") {
if (entityState.state !== "idle") {
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
}
@@ -80,7 +80,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
To make sure the wake word works for you.
</p>`}
</div>
<div class="footer">
<div class="footer full-width">
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
</div>`;
}
+3 -2
View File
@@ -141,9 +141,10 @@ interface EMOutgoingMessageImprovScan extends EMMessage {
interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
type: "thread/store_in_platform_keychain";
payload: {
mac_extended_address: string;
border_agent_id: string;
mac_extended_address: string | null;
border_agent_id: string | null;
active_operational_dataset: string;
extended_pan_id: string;
};
}
@@ -5,12 +5,13 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-circular-progress";
import "../../../components/ha-combo-box";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-markdown";
import "../../../components/ha-password-field";
import "../../../components/ha-textfield";
import "../../../components/ha-button";
import {
ApplicationCredential,
ApplicationCredentialsConfig,
@@ -208,11 +209,10 @@ export class DialogAddApplicationCredential extends LitElement {
)}
helperPersistent
></ha-textfield>
<ha-textfield
<ha-password-field
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret"
)}
type="password"
name="clientSecret"
.value=${this._clientSecret}
required
@@ -222,7 +222,7 @@ export class DialogAddApplicationCredential extends LitElement {
"ui.panel.config.application_credentials.editor.client_secret_helper"
)}
helperPersistent
></ha-textfield>
></ha-password-field>
</div>
${this._loading
? html`
@@ -43,13 +43,8 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../../../data/context";
import { fullEntitiesContext, labelsContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { FloorRegistryEntry } from "../../../../data/floor_registry";
import { LabelRegistryEntry } from "../../../../data/label_registry";
import {
Action,
@@ -159,10 +154,6 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: FloorRegistryEntry[];
@state() private _warnings?: string[];
@state() private _uiModeAvailable = true;
@@ -231,7 +222,6 @@ export default class HaAutomationActionRow extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action
)
)}
@@ -603,7 +593,6 @@ export default class HaAutomationActionRow extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action,
undefined,
true
@@ -156,6 +156,15 @@ export default class HaAutomationAction extends LitElement {
}
}
public expandAll() {
const rows = this.shadowRoot!.querySelectorAll<HaAutomationActionRow>(
"ha-automation-action-row"
)!;
rows.forEach((row) => {
row.expand();
});
}
private _addActionDialog() {
showAddAutomationElementDialog(this, {
type: "action",
@@ -55,12 +55,12 @@ export class HaSceneAction extends LitElement implements ActionElement {
fireEvent(this, "value-changed", {
value: {
...this.action,
service: "scene.turn_on",
action: "scene.turn_on",
target: {
entity_id: ev.detail.value,
},
metadata: {},
},
} as SceneAction,
});
}
}
@@ -52,7 +52,7 @@ export class HaPlayMediaAction extends LitElement implements ActionElement {
fireEvent(this, "value-changed", {
value: {
...this.action,
service: "media_player.play_media",
action: "media_player.play_media",
target: { entity_id: ev.detail.value.entity_id },
data: {
media_content_id: ev.detail.value.media_content_id,
@@ -117,6 +117,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
.value=${this._action}
.disabled=${this.disabled}
.showAdvanced=${this.hass.userData?.showAdvanced}
.hidePicker=${!!this._action.metadata}
@value-changed=${this._actionChanged}
></ha-service-control>
${domain && service && this.hass.services[domain]?.[service]?.response
@@ -208,6 +208,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
const options: IFuseOptions<ListItem> = {
keys: ["key", "name", "description"],
isCaseSensitive: false,
ignoreLocation: true,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
getFn: getStripDiacriticsFn,
@@ -41,7 +41,9 @@ class DialogAutomationRename extends LitElement implements HassDialog {
this._newIcon = "icon" in params.config ? params.config.icon : undefined;
this._newName =
params.config.alias ||
this.hass.localize("ui.panel.config.automation.editor.default_name");
this.hass.localize(
`ui.panel.config.${this._params.domain}.editor.default_name`
);
this._newDescription = params.config.description || "";
}
@@ -83,7 +85,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
dialogInitialFocus
.value=${this._newName}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.default_name"
`ui.panel.config.${this._params.domain}.editor.default_name`
)}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
@@ -106,6 +106,15 @@ export default class HaAutomationCondition extends LitElement {
}
}
public expandAll() {
const rows = this.shadowRoot!.querySelectorAll<HaAutomationConditionRow>(
"ha-automation-condition-row"
)!;
rows.forEach((row) => {
row.expand();
});
}
private get nested() {
return this.path !== undefined;
}
@@ -8,13 +8,21 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import "../../../../../components/ha-select";
import type {
AutomationConfig,
Trigger,
TriggerCondition,
import {
flattenTriggers,
type AutomationConfig,
type Trigger,
type TriggerCondition,
} from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
const getTriggersIds = (triggers: Trigger[]): string[] => {
const triggerIds = flattenTriggers(triggers)
.map((t) => ("id" in t ? t.id : undefined))
.filter(Boolean) as string[];
return Array.from(new Set(triggerIds));
};
@customElement("ha-automation-condition-trigger")
export class HaTriggerCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -23,7 +31,7 @@ export class HaTriggerCondition extends LitElement {
@property({ type: Boolean }) public disabled = false;
@state() private _triggers: Trigger[] = [];
@state() private _triggerIds: string[] = [];
private _unsub?: UnsubscribeFunc;
@@ -35,14 +43,14 @@ export class HaTriggerCondition extends LitElement {
}
private _schema = memoizeOne(
(triggers: Trigger[]) =>
(triggerIds: string[]) =>
[
{
name: "id",
selector: {
select: {
multiple: true,
options: triggers.map((trigger) => trigger.id!),
options: triggerIds,
},
},
required: true,
@@ -65,13 +73,13 @@ export class HaTriggerCondition extends LitElement {
}
protected render() {
if (!this._triggers.length) {
if (!this._triggerIds.length) {
return this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
);
}
const schema = this._schema(this._triggers);
const schema = this._schema(this._triggerIds);
return html`
<ha-form
@@ -93,11 +101,8 @@ export class HaTriggerCondition extends LitElement {
);
private _automationUpdated(config?: AutomationConfig) {
const seenIds = new Set();
this._triggers = config?.trigger
? ensureArray(config.trigger).filter(
(t) => t.id && (seenIds.has(t.id) ? false : seenIds.add(t.id))
)
this._triggerIds = config?.triggers
? getTriggersIds(ensureArray(config.triggers))
: [];
}
@@ -106,12 +111,12 @@ export class HaTriggerCondition extends LitElement {
const newValue = ev.detail.value;
if (typeof newValue.id === "string") {
if (!this._triggers.some((trigger) => trigger.id === newValue.id)) {
if (!this._triggerIds.some((id) => id === newValue.id)) {
newValue.id = "";
}
} else if (Array.isArray(newValue.id)) {
newValue.id = newValue.id.filter((id) =>
this._triggers.some((trigger) => trigger.id === id)
newValue.id = newValue.id.filter((_id) =>
this._triggerIds.some((id) => id === _id)
);
if (!newValue.id.length) {
newValue.id = "";
@@ -1,7 +1,14 @@
import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../../common/array/ensure-array";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -21,6 +28,14 @@ import { documentationUrl } from "../../../util/documentation-url";
import "./action/ha-automation-action";
import "./condition/ha-automation-condition";
import "./trigger/ha-automation-trigger";
import type HaAutomationTrigger from "./trigger/ha-automation-trigger";
import type HaAutomationAction from "./action/ha-automation-action";
import type HaAutomationCondition from "./condition/ha-automation-condition";
import {
extractSearchParam,
removeSearchParam,
} from "../../../common/url/search-params";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
@customElement("manual-automation-editor")
export class HaManualAutomationEditor extends LitElement {
@@ -36,6 +51,31 @@ export class HaManualAutomationEditor extends LitElement {
@property({ attribute: false }) public stateObj?: HassEntity;
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
const expanded = extractSearchParam("expanded");
if (expanded === "1") {
this._clearParam("expanded");
const items = this.shadowRoot!.querySelectorAll<
HaAutomationTrigger | HaAutomationCondition | HaAutomationAction
>("ha-automation-trigger, ha-automation-condition, ha-automation-action");
items.forEach((el) => {
el.updateComplete.then(() => {
el.expandAll();
});
});
}
}
private _clearParam(param: string) {
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam(param))
);
}
protected render() {
return html`
${this.stateObj?.state === "off"
@@ -78,7 +118,7 @@ export class HaManualAutomationEditor extends LitElement {
></ha-icon-button>
</a>
</div>
${!ensureArray(this.config.trigger)?.length
${!ensureArray(this.config.triggers)?.length
? html`<p>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.description"
@@ -29,6 +29,7 @@ import { classMap } from "lit/directives/class-map";
import { storage } from "../../../../common/decorators/storage";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefault } from "../../../../common/dom/prevent_default";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
@@ -50,7 +51,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { TRIGGER_ICONS } from "../../../../data/trigger";
import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger";
import {
showAlertDialog,
showConfirmationDialog,
@@ -64,6 +65,7 @@ import "./types/ha-automation-trigger-device";
import "./types/ha-automation-trigger-event";
import "./types/ha-automation-trigger-geo_location";
import "./types/ha-automation-trigger-homeassistant";
import "./types/ha-automation-trigger-list";
import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-persistent_notification";
@@ -75,7 +77,6 @@ import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
import { preventDefault } from "../../../../common/dom/prevent_default";
export interface TriggerElement extends LitElement {
trigger: Trigger;
@@ -87,7 +88,7 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => {
if (!name) {
return;
}
const newVal = (ev.target as any)?.value;
const newVal = ev.detail?.value || (ev.currentTarget as any)?.value;
if ((element.trigger[name] || "") === newVal) {
return;
@@ -146,15 +147,17 @@ export default class HaAutomationTriggerRow extends LitElement {
protected render() {
if (!this.trigger) return nothing;
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
const supported =
customElements.get(`ha-automation-trigger-${this.trigger.trigger}`) !==
undefined;
customElements.get(`ha-automation-trigger-${type}`) !== undefined;
const yamlMode = this._yamlMode || !supported;
const showId = "id" in this.trigger || this._requestShowId;
return html`
<ha-card outlined>
${this.trigger.enabled === false
${"enabled" in this.trigger && this.trigger.enabled === false
? html`
<div class="disabled-bar">
${this.hass.localize(
@@ -168,7 +171,7 @@ export default class HaAutomationTriggerRow extends LitElement {
<h3 slot="header">
<ha-svg-icon
class="trigger-icon"
.path=${TRIGGER_ICONS[this.trigger.trigger]}
.path=${TRIGGER_ICONS[type]}
></ha-svg-icon>
${describeTrigger(this.trigger, this.hass, this._entityReg)}
</h3>
@@ -188,14 +191,20 @@ export default class HaAutomationTriggerRow extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
<mwc-list-item
graphic="icon"
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</mwc-list-item>
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
<mwc-list-item
graphic="icon"
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
@@ -274,8 +283,11 @@ export default class HaAutomationTriggerRow extends LitElement {
<li divider role="separator"></li>
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
${this.trigger.enabled === false
<mwc-list-item
graphic="icon"
.disabled=${this.disabled || type === "list"}
>
${"enabled" in this.trigger && this.trigger.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
@@ -284,7 +296,8 @@ export default class HaAutomationTriggerRow extends LitElement {
)}
<ha-svg-icon
slot="graphic"
.path=${this.trigger.enabled === false
.path=${"enabled" in this.trigger &&
this.trigger.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
@@ -308,7 +321,8 @@ export default class HaAutomationTriggerRow extends LitElement {
<div
class=${classMap({
"card-content": true,
disabled: this.trigger.enabled === false,
disabled:
"enabled" in this.trigger && this.trigger.enabled === false,
})}
>
${this._warnings
@@ -336,7 +350,7 @@ export default class HaAutomationTriggerRow extends LitElement {
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
{ platform: this.trigger.trigger }
{ platform: type }
)}
`
: ""}
@@ -348,7 +362,7 @@ export default class HaAutomationTriggerRow extends LitElement {
></ha-yaml-editor>
`
: html`
${showId
${showId && !isTriggerList(this.trigger)
? html`
<ha-textfield
.label=${this.hass.localize(
@@ -365,15 +379,12 @@ export default class HaAutomationTriggerRow extends LitElement {
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@value-changed=${this._onUiChanged}
>
${dynamicElement(
`ha-automation-trigger-${this.trigger.trigger}`,
{
hass: this.hass,
trigger: this.trigger,
disabled: this.disabled,
path: this.path,
}
)}
${dynamicElement(`ha-automation-trigger-${type}`, {
hass: this.hass,
trigger: this.trigger,
disabled: this.disabled,
path: this.path,
})}
</div>
`}
</div>
@@ -546,6 +557,7 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _onDisable() {
if (isTriggerList(this.trigger)) return;
const enabled = !(this.trigger.enabled ?? true);
const value = { ...this.trigger, enabled };
fireEvent(this, "value-changed", { value });
@@ -555,7 +567,9 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _idChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
const newId = (ev.target as any).value;
if (newId === (this.trigger.id ?? "")) {
return;
}
@@ -583,6 +597,7 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _onUiChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
ev.stopPropagation();
const value = {
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
@@ -617,6 +632,7 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private async _renameTrigger(): Promise<void> {
if (isTriggerList(this.trigger)) return;
const alias = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.triggers.change_alias"
@@ -18,7 +18,11 @@ import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import { AutomationClipboard, Trigger } from "../../../../data/automation";
import {
AutomationClipboard,
Trigger,
TriggerList,
} from "../../../../data/automation";
import { HomeAssistant, ItemPath } from "../../../../types";
import {
PASTE_VALUE,
@@ -26,6 +30,7 @@ import {
} from "../show-add-automation-element-dialog";
import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import { isTriggerList } from "../../../../data/trigger";
@customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends LitElement {
@@ -130,7 +135,11 @@ export default class HaAutomationTrigger extends LitElement {
showAddAutomationElementDialog(this, {
type: "trigger",
add: this._addTrigger,
clipboardItem: this._clipboard?.trigger?.trigger,
clipboardItem: !this._clipboard?.trigger
? undefined
: isTriggerList(this._clipboard.trigger)
? "list"
: this._clipboard?.trigger?.trigger,
});
}
@@ -139,7 +148,7 @@ export default class HaAutomationTrigger extends LitElement {
if (value === PASTE_VALUE) {
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
} else {
const trigger = value as Trigger["trigger"];
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
const elClass = customElements.get(
`ha-automation-trigger-${trigger}`
) as CustomElementConstructor & {
@@ -170,6 +179,15 @@ export default class HaAutomationTrigger extends LitElement {
}
}
public expandAll() {
const rows = this.shadowRoot!.querySelectorAll<HaAutomationTriggerRow>(
"ha-automation-trigger-row"
)!;
rows.forEach((row) => {
row.expand();
});
}
private _getKey(action: Trigger) {
if (!this._triggerKeys.has(action)) {
this._triggerKeys.set(action, Math.random().toString());
@@ -0,0 +1,54 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../../../../common/array/ensure-array";
import type { TriggerList } from "../../../../../data/automation";
import type { HomeAssistant, ItemPath } from "../../../../../types";
import "../ha-automation-trigger";
import {
handleChangeEvent,
TriggerElement,
} from "../ha-automation-trigger-row";
@customElement("ha-automation-trigger-list")
export class HaTriggerList extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trigger!: TriggerList;
@property({ attribute: false }) public path?: ItemPath;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): TriggerList {
return {
triggers: [],
};
}
protected render() {
const triggers = ensureArray(this.trigger.triggers);
return html`
<ha-automation-trigger
.path=${[...(this.path ?? []), "triggers"]}
.triggers=${triggers}
.hass=${this.hass}
.disabled=${this.disabled}
.name=${"triggers"}
@value-changed=${this._valueChanged}
></ha-automation-trigger>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
static styles = css``;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-list": HaTriggerList;
}
}
@@ -9,6 +9,9 @@ import type { TimeTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import type { TriggerElement } from "../ha-automation-trigger-row";
const MODE_TIME = "time";
const MODE_ENTITY = "entity";
@customElement("ha-automation-trigger-time")
export class HaTimeTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -17,48 +20,60 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
@property({ type: Boolean }) public disabled = false;
@state() private _inputMode?: boolean;
@state() private _inputMode:
| undefined
| typeof MODE_TIME
| typeof MODE_ENTITY;
public static get defaultConfig(): TimeTrigger {
return { trigger: "time", at: "" };
}
private _schema = memoizeOne(
(localize: LocalizeFunc, inputMode?: boolean) => {
const atSelector = inputMode
? {
entity: {
filter: [
{ domain: "input_datetime" },
{ domain: "sensor", device_class: "timestamp" },
],
},
}
: { time: {} };
return [
(
localize: LocalizeFunc,
inputMode: typeof MODE_TIME | typeof MODE_ENTITY,
showOffset: boolean
) =>
[
{
name: "mode",
type: "select",
required: true,
options: [
[
"value",
MODE_TIME,
localize(
"ui.panel.config.automation.editor.triggers.type.time.type_value"
),
],
[
"input",
MODE_ENTITY,
localize(
"ui.panel.config.automation.editor.triggers.type.time.type_input"
),
],
],
},
{ name: "at", selector: atSelector },
] as const;
}
...(inputMode === MODE_TIME
? ([{ name: "time", selector: { time: {} } }] as const)
: ([
{
name: "entity",
selector: {
entity: {
filter: [
{ domain: "input_datetime" },
{ domain: "sensor", device_class: "timestamp" },
],
},
},
},
] as const)),
...(showOffset
? ([{ name: "offset", selector: { text: {} } }] as const)
: ([] as const)),
] as const
);
public willUpdate(changedProperties: PropertyValues) {
@@ -75,23 +90,46 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
}
}
private _data = memoizeOne(
(
inputMode: undefined | typeof MODE_ENTITY | typeof MODE_TIME,
at:
| string
| { entity_id: string | undefined; offset?: string | undefined }
): {
mode: typeof MODE_TIME | typeof MODE_ENTITY;
entity: string | undefined;
time: string | undefined;
offset: string | undefined;
} => {
const entity =
typeof at === "object"
? at.entity_id
: at?.startsWith("input_datetime.") || at?.startsWith("sensor.")
? at
: undefined;
const time = entity ? undefined : (at as string | undefined);
const offset = typeof at === "object" ? at.offset : undefined;
const mode = inputMode ?? (entity ? MODE_ENTITY : MODE_TIME);
return {
mode,
entity,
time,
offset,
};
}
);
protected render() {
const at = this.trigger.at;
if (Array.isArray(at)) {
return nothing;
}
const inputMode =
this._inputMode ??
(at?.startsWith("input_datetime.") || at?.startsWith("sensor."));
const schema = this._schema(this.hass.localize, inputMode);
const data = {
mode: inputMode ? "input" : "value",
...this.trigger,
};
const data = this._data(this._inputMode, at);
const showOffset =
data.mode === MODE_ENTITY && data.entity?.startsWith("sensor.");
const schema = this._schema(this.hass.localize, data.mode, !!showOffset);
return html`
<ha-form
@@ -107,26 +145,43 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newValue = ev.detail.value;
this._inputMode = newValue.mode === "input";
delete newValue.mode;
Object.keys(newValue).forEach((key) =>
newValue[key] === undefined || newValue[key] === ""
? delete newValue[key]
: {}
);
fireEvent(this, "value-changed", { value: newValue });
const newValue = { ...ev.detail.value };
this._inputMode = newValue.mode;
if (newValue.mode === MODE_TIME) {
delete newValue.entity;
delete newValue.offset;
} else {
delete newValue.time;
if (!newValue.entity?.startsWith("sensor.")) {
delete newValue.offset;
}
}
fireEvent(this, "value-changed", {
value: {
...this.trigger,
at: newValue.offset
? {
entity_id: newValue.entity,
offset: newValue.offset,
}
: newValue.entity || newValue.time,
},
});
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
): string => {
switch (schema.name) {
case "time":
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time.at`
);
}
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time.${schema.name}`
);
};
}
declare global {
@@ -99,24 +99,32 @@ export class CloudForgotPassword extends LitElement {
this._requestInProgress = true;
try {
await cloudForgotPassword(this.hass, email);
// @ts-ignore
fireEvent(this, "email-changed", { value: email });
this._requestInProgress = false;
// @ts-ignore
fireEvent(this, "cloud-done", {
flashMessage: this.hass.localize(
"ui.panel.config.cloud.forgot_password.check_your_email"
),
});
} catch (err: any) {
this._requestInProgress = false;
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
const doResetPassword = async (username: string) => {
try {
await cloudForgotPassword(this.hass, username);
// @ts-ignore
fireEvent(this, "email-changed", { value: username });
this._requestInProgress = false;
// @ts-ignore
fireEvent(this, "cloud-done", {
flashMessage: this.hass.localize(
"ui.panel.config.cloud.forgot_password.check_your_email"
),
});
} catch (err: any) {
this._requestInProgress = false;
const errCode = err && err.body && err.body.code;
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
await doResetPassword(username.toLowerCase());
} else {
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
}
};
await doResetPassword(email);
}
static get styles() {
+58 -49
View File
@@ -1,8 +1,8 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list";
import { mdiDeleteForever, mdiDotsVertical } from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { mdiDeleteForever, mdiDotsVertical } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import { navigate } from "../../../../common/navigate";
import "../../../../components/buttons/ha-progress-button";
@@ -10,8 +10,11 @@ import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-list-item";
import type { HaTextField } from "../../../../components/ha-textfield";
import "../../../../components/ha-password-field";
import type { HaPasswordField } from "../../../../components/ha-password-field";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
import { cloudLogin, removeCloudData } from "../../../../data/cloud";
import {
showAlertDialog,
@@ -21,7 +24,6 @@ import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
@customElement("cloud-login")
export class CloudLogin extends LitElement {
@@ -43,7 +45,7 @@ export class CloudLogin extends LitElement {
@query("#email", true) private _emailField!: HaTextField;
@query("#password", true) private _passwordField!: HaTextField;
@query("#password", true) private _passwordField!: HaPasswordField;
protected render(): TemplateResult {
return html`
@@ -142,14 +144,13 @@ export class CloudLogin extends LitElement {
"ui.panel.config.cloud.login.email_error_msg"
)}
></ha-textfield>
<ha-textfield
<ha-password-field
id="password"
name="password"
.label=${this.hass.localize(
"ui.panel.config.cloud.login.password"
)}
.value=${this._password || ""}
type="password"
autocomplete="current-password"
required
minlength="8"
@@ -158,7 +159,7 @@ export class CloudLogin extends LitElement {
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.login.password_error_msg"
)}
></ha-textfield>
></ha-password-field>
</div>
<div class="card-actions">
<ha-progress-button
@@ -227,53 +228,61 @@ export class CloudLogin extends LitElement {
this._requestInProgress = true;
try {
const result = await cloudLogin(this.hass, email, password);
fireEvent(this, "ha-refresh-cloud-status");
this.email = "";
this._password = "";
if (result.cloud_pipeline) {
if (
await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.login.cloud_pipeline_title"
),
text: this.hass.localize(
"ui.panel.config.cloud.login.cloud_pipeline_text"
),
})
) {
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
const doLogin = async (username: string) => {
try {
const result = await cloudLogin(this.hass, username, password);
fireEvent(this, "ha-refresh-cloud-status");
this.email = "";
this._password = "";
if (result.cloud_pipeline) {
if (
await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.login.cloud_pipeline_title"
),
text: this.hass.localize(
"ui.panel.config.cloud.login.cloud_pipeline_text"
),
})
) {
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
}
}
} catch (err: any) {
const errCode = err && err.body && err.body.code;
if (errCode === "PasswordChangeRequired") {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.login.alert_password_change_required"
),
});
navigate("/config/cloud/forgot-password");
return;
}
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
await doLogin(username.toLowerCase());
return;
}
}
} catch (err: any) {
const errCode = err && err.body && err.body.code;
if (errCode === "PasswordChangeRequired") {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.login.alert_password_change_required"
),
});
navigate("/config/cloud/forgot-password");
return;
}
this._password = "";
this._requestInProgress = false;
this._password = "";
this._requestInProgress = false;
if (errCode === "UserNotConfirmed") {
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
);
} else {
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
if (errCode === "UserNotConfirmed") {
this._error = this.hass.localize(
"ui.panel.config.cloud.login.alert_email_confirm_necessary"
);
} else {
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
emailField.focus();
}
};
emailField.focus();
}
await doLogin(email);
}
private _handleRegister() {
@@ -11,6 +11,7 @@ import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import "../../../../components/ha-password-field";
@customElement("cloud-register")
export class CloudRegister extends LitElement {
@@ -145,14 +146,13 @@ export class CloudRegister extends LitElement {
"ui.panel.config.cloud.register.email_error_msg"
)}
></ha-textfield>
<ha-textfield
<ha-password-field
id="password"
name="password"
.label=${this.hass.localize(
"ui.panel.config.cloud.register.password"
)}
.value=${this._password}
type="password"
autocomplete="new-password"
minlength="8"
required
@@ -160,7 +160,7 @@ export class CloudRegister extends LitElement {
validationMessage=${this.hass.localize(
"ui.panel.config.cloud.register.password_error_msg"
)}
></ha-textfield>
></ha-password-field>
</div>
<div class="card-actions">
<ha-progress-button
@@ -197,9 +197,6 @@ export class CloudRegister extends LitElement {
const emailField = this._emailField;
const passwordField = this._passwordField;
const email = emailField.value;
const password = passwordField.value;
if (!emailField.reportValidity()) {
passwordField.reportValidity();
emailField.focus();
@@ -211,6 +208,9 @@ export class CloudRegister extends LitElement {
return;
}
const email = emailField.value.toLowerCase();
const password = passwordField.value;
this._requestInProgress = true;
try {
@@ -229,22 +229,31 @@ export class CloudRegister extends LitElement {
private async _handleResendVerifyEmail() {
const emailField = this._emailField;
const email = emailField.value;
if (!emailField.reportValidity()) {
emailField.focus();
return;
}
try {
await cloudResendVerification(this.hass, email);
this._verificationEmailSent(email);
} catch (err: any) {
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
const email = emailField.value;
const doResend = async (username: string) => {
try {
await cloudResendVerification(this.hass, username);
this._verificationEmailSent(username);
} catch (err: any) {
const errCode = err && err.body && err.body.code;
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
await doResend(username.toLowerCase());
} else {
this._error =
err && err.body && err.body.message
? err.body.message
: "Unknown error";
}
}
};
await doResend(email);
}
private _verificationEmailSent(email: string) {
@@ -1,23 +0,0 @@
import { customElement } from "lit/decorators";
import {
DeviceAction,
localizeDeviceAutomationAction,
} from "../../../../data/device_automation";
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
@customElement("ha-device-actions-card")
export class HaDeviceActionsCard extends HaDeviceAutomationCard<DeviceAction> {
readonly type = "action";
readonly headerKey = "ui.panel.config.devices.automation.actions.caption";
constructor() {
super(localizeDeviceAutomationAction);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-actions-card": HaDeviceActionsCard;
}
}
@@ -1,142 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/chips/ha-assist-chip";
import "../../../../components/chips/ha-chip-set";
import { showAutomationEditor } from "../../../../data/automation";
import {
DeviceAction,
DeviceAutomation,
} from "../../../../data/device_automation";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { showScriptEditor } from "../../../../data/script";
import { buttonLinkStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
declare global {
interface HASSDomEvents {
"entry-selected": undefined;
}
}
export abstract class HaDeviceAutomationCard<
T extends DeviceAutomation,
> extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public deviceId?: string;
@property({ type: Boolean }) public script = false;
@property({ attribute: false }) public automations: T[] = [];
@property({ attribute: false }) entityReg?: EntityRegistryEntry[];
@state() public _showSecondary = false;
abstract headerKey: Parameters<typeof this.hass.localize>[0];
abstract type: "action" | "condition" | "trigger";
private _localizeDeviceAutomation: (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
automation: T
) => string;
constructor(
localizeDeviceAutomation: HaDeviceAutomationCard<T>["_localizeDeviceAutomation"]
) {
super();
this._localizeDeviceAutomation = localizeDeviceAutomation;
}
protected shouldUpdate(changedProps): boolean {
if (changedProps.has("deviceId") || changedProps.has("automations")) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
return true;
}
return false;
}
protected render() {
if (this.automations.length === 0 || !this.entityReg) {
return nothing;
}
const automations = this._showSecondary
? this.automations
: this.automations.filter(
(automation) => automation.metadata?.secondary === false
);
return html`
<h3>${this.hass.localize(this.headerKey)}</h3>
<div class="content">
<ha-chip-set>
${automations.map(
(automation, idx) => html`
<ha-assist-chip
filled
.index=${idx}
@click=${this._handleAutomationClicked}
class=${automation.metadata?.secondary ? "secondary" : ""}
.label=${this._localizeDeviceAutomation(
this.hass,
this.entityReg!,
automation
)}
>
</ha-assist-chip>
`
)}
</ha-chip-set>
${!this._showSecondary && automations.length < this.automations.length
? html`<button class="link" @click=${this._toggleSecondary}>
Show ${this.automations.length - automations.length} more...
</button>`
: ""}
</div>
`;
}
private _toggleSecondary() {
this._showSecondary = !this._showSecondary;
}
private _handleAutomationClicked(ev: CustomEvent) {
const automation = { ...this.automations[(ev.currentTarget as any).index] };
if (!automation) {
return;
}
delete automation.metadata;
if (this.script) {
showScriptEditor({ sequence: [automation as DeviceAction] });
fireEvent(this, "entry-selected");
return;
}
const data = {};
data[this.type] = [automation];
showAutomationEditor(data);
fireEvent(this, "entry-selected");
}
static styles = [
buttonLinkStyle,
css`
h3 {
color: var(--primary-text-color);
}
.secondary {
--ha-assist-chip-filled-container-color: rgba(
var(--rgb-primary-text-color),
0.07
);
}
button.link {
color: var(--primary-color);
}
`,
];
}
@@ -1,8 +1,18 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import {
mdiAbTesting,
mdiGestureTap,
mdiPencilOutline,
mdiRoomService,
} from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-dialog";
import { shouldHandleRequestSelectedEvent } from "../../../../common/mwc/handle-request-selected-event";
import { createCloseHeading } from "../../../../components/ha-dialog";
import {
AutomationConfig,
showAutomationEditor,
} from "../../../../data/automation";
import {
DeviceAction,
DeviceCondition,
@@ -12,11 +22,9 @@ import {
fetchDeviceTriggers,
sortDeviceAutomations,
} from "../../../../data/device_automation";
import { haStyleDialog } from "../../../../resources/styles";
import { ScriptConfig, showScriptEditor } from "../../../../data/script";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "./ha-device-actions-card";
import "./ha-device-conditions-card";
import "./ha-device-triggers-card";
import { DeviceAutomationDialogParams } from "./show-dialog-device-automation";
@customElement("dialog-device-automation")
@@ -77,75 +85,184 @@ export class DialogDeviceAutomation extends LitElement {
});
}
private _handleRowClick = (ev) => {
if (!shouldHandleRequestSelectedEvent(ev) || !this._params) {
return;
}
const type = (ev.currentTarget as any).type;
const isScript = this._params.script;
this.closeDialog();
if (isScript) {
const newScript = {} as ScriptConfig;
if (type === "action") {
newScript.sequence = [this._actions[0]];
}
showScriptEditor(newScript, true);
} else {
const newAutomation = {} as AutomationConfig;
if (type === "trigger") {
newAutomation.triggers = [this._triggers[0]];
}
if (type === "condition") {
newAutomation.conditions = [this._conditions[0]];
}
if (type === "action") {
newAutomation.actions = [this._actions[0]];
}
showAutomationEditor(newAutomation, true);
}
};
protected render() {
if (!this._params) {
return nothing;
}
const mode = this._params.script ? "script" : "automation";
const title = this.hass.localize(`ui.panel.config.devices.${mode}.create`, {
type: this.hass.localize(
`ui.panel.config.devices.type.${
this._params.device.entry_type || "device"
}`
),
});
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${this.hass.localize(
`ui.panel.config.devices.${
this._params.script ? "script" : "automation"
}.create`,
{
type: this.hass.localize(
`ui.panel.config.devices.type.${
this._params.device.entry_type || "device"
}`
),
}
)}
.heading=${createCloseHeading(this.hass, title)}
>
<div @entry-selected=${this.closeDialog}>
<mwc-list
innerRole="listbox"
itemRoles="option"
innerAriaLabel="Create new automation"
rootTabbable
dialogInitialFocus
>
${this._triggers.length
? html`
<ha-list-item
hasmeta
twoline
graphic="icon"
.type=${"trigger"}
@request-selected=${this._handleRowClick}
>
<ha-svg-icon
slot="graphic"
.path=${mdiGestureTap}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.devices.automation.triggers.title`
)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.automation.triggers.description`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing}
${this._conditions.length
? html`
<ha-list-item
hasmeta
twoline
graphic="icon"
.type=${"condition"}
@request-selected=${this._handleRowClick}
>
<ha-svg-icon
slot="graphic"
.path=${mdiAbTesting}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.devices.automation.conditions.title`
)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.automation.conditions.description`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing}
${this._actions.length
? html`
<ha-list-item
hasmeta
twoline
graphic="icon"
.type=${"action"}
@request-selected=${this._handleRowClick}
>
<ha-svg-icon
slot="graphic"
.path=${mdiRoomService}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.devices.${mode}.actions.title`
)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.${mode}.actions.description`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing}
${this._triggers.length ||
this._conditions.length ||
this._actions.length
? html`
${this._triggers.length
? html`
<ha-device-triggers-card
.hass=${this.hass}
.automations=${this._triggers}
.entityReg=${this._params.entityReg}
></ha-device-triggers-card>
`
: ""}
${this._conditions.length
? html`
<ha-device-conditions-card
.hass=${this.hass}
.automations=${this._conditions}
.entityReg=${this._params.entityReg}
></ha-device-conditions-card>
`
: ""}
${this._actions.length
? html`
<ha-device-actions-card
.hass=${this.hass}
.automations=${this._actions}
.script=${this._params.script}
.entityReg=${this._params.entityReg}
></ha-device-actions-card>
`
: ""}
`
: this.hass.localize(
"ui.panel.config.devices.automation.no_device_automations"
? html`<li divider role="separator"></li>`
: nothing}
<ha-list-item
hasmeta
twoline
graphic="icon"
@request-selected=${this._handleRowClick}
>
<ha-svg-icon slot="graphic" .path=${mdiPencilOutline}></ha-svg-icon>
${this.hass.localize(`ui.panel.config.devices.${mode}.new.title`)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.${mode}.new.description`
)}
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</mwc-list>
</ha-dialog>
`;
}
static get styles(): CSSResultGroup {
return haStyleDialog;
return [
haStyle,
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
--mdc-dialog-max-height: 60vh;
}
@media all and (min-width: 550px) {
ha-dialog {
--mdc-dialog-min-width: 500px;
}
}
ha-icon-next {
width: 24px;
}
`,
];
}
}
@@ -1,23 +0,0 @@
import { customElement } from "lit/decorators";
import {
DeviceCondition,
localizeDeviceAutomationCondition,
} from "../../../../data/device_automation";
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
@customElement("ha-device-conditions-card")
export class HaDeviceConditionsCard extends HaDeviceAutomationCard<DeviceCondition> {
readonly type = "condition";
readonly headerKey = "ui.panel.config.devices.automation.conditions.caption";
constructor() {
super(localizeDeviceAutomationCondition);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-conditions-card": HaDeviceConditionsCard;
}
}
@@ -1,23 +0,0 @@
import { customElement } from "lit/decorators";
import {
DeviceTrigger,
localizeDeviceAutomationTrigger,
} from "../../../../data/device_automation";
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
@customElement("ha-device-triggers-card")
export class HaDeviceTriggersCard extends HaDeviceAutomationCard<DeviceTrigger> {
readonly type = "trigger";
readonly headerKey = "ui.panel.config.devices.automation.triggers.caption";
constructor() {
super(localizeDeviceAutomationTrigger);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-triggers-card": HaDeviceTriggersCard;
}
}
@@ -17,6 +17,30 @@ import type { DeviceAction } from "../../../ha-config-device-page";
import { showMatterManageFabricsDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-manage-fabrics";
import { navigate } from "../../../../../../common/navigate";
export const getMatterDeviceDefaultActions = (
el: HTMLElement,
hass: HomeAssistant,
device: DeviceRegistryEntry
): DeviceAction[] => {
if (device.via_device_id !== null) {
// only show device actions for top level nodes (so not bridged)
return [];
}
const actions: DeviceAction[] = [];
actions.push({
label: hass.localize("ui.panel.config.matter.device_actions.ping_device"),
icon: mdiChatQuestion,
action: () =>
showMatterPingNodeDialog(el, {
device_id: device.id,
}),
});
return actions;
};
export const getMatterDeviceActions = async (
el: HTMLElement,
hass: HomeAssistant,
@@ -75,14 +99,5 @@ export const getMatterDeviceActions = async (
});
}
actions.push({
label: hass.localize("ui.panel.config.matter.device_actions.ping_device"),
icon: mdiChatQuestion,
action: () =>
showMatterPingNodeDialog(el, {
device_id: device.id,
}),
});
return actions;
};
@@ -83,9 +83,15 @@ export const getZHADeviceActions = async (
classes: "warning",
action: async () => {
const confirmed = await showConfirmationDialog(el, {
text: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove"
title: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove_title"
),
text: hass.localize(
"ui.dialogs.zha_device_info.confirmations.remove_text"
),
confirmText: hass.localize("ui.common.remove"),
dismissText: hass.localize("ui.common.cancel"),
destructive: true,
});
if (!confirmed) {
@@ -35,6 +35,7 @@ import "../../../components/ha-button-menu";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-svg-icon";
import "../../../components/ha-expansion-panel";
import { getSignedPath } from "../../../data/auth";
import {
ConfigEntry,
@@ -1119,12 +1120,17 @@ export class HaConfigDevicePage extends LitElement {
const matter = await import(
"./device-detail/integration-elements/matter/device-actions"
);
const actions = await matter.getMatterDeviceActions(
const defaultActions = matter.getMatterDeviceDefaultActions(
this,
this.hass,
device
);
deviceActions.push(...actions);
deviceActions.push(...defaultActions);
// load matter device actions async to avoid an UI with 0 actions when the matter integration needs very long to get node diagnostics
matter.getMatterDeviceActions(this, this.hass, device).then((actions) => {
this._deviceActions = [...actions, ...(this._deviceActions || [])];
});
}
this._deviceActions = deviceActions;
@@ -1349,16 +1355,14 @@ export class HaConfigDevicePage extends LitElement {
.filter((entity) => entity.newId)
.map(
(entity) =>
html`<li style="white-space: nowrap;">
${entity.oldId} -> ${entity.newId}
</li>`
html`<tr>
<td>${entity.oldId}</td>
<td>${entity.newId}</td>
</tr>`
);
const dialogNoRenames = entityIdRenames
.filter((entity) => !entity.newId)
.map(
(entity) =>
html`<li style="white-space: nowrap;">${entity.oldId}</li>`
);
.map((entity) => html`<li>${entity.oldId}</li>`);
if (dialogRenames.length) {
renameEntityid = await showConfirmationDialog(this, {
@@ -1367,17 +1371,46 @@ export class HaConfigDevicePage extends LitElement {
),
text: html`${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_ids_warning"
)} <br /><br />${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_will_rename"
)}:
${dialogRenames}
)} <br /><br />
<ha-expansion-panel outlined>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_will_rename",
{ count: dialogRenames.length }
)}</span
>
<div style="overflow: auto;">
<table style="width: 100%; text-align: var(--float-start);">
<tr>
<th>
${this.hass.localize(
"ui.panel.config.devices.confirm_rename_old"
)}
</th>
<th>
${this.hass.localize(
"ui.panel.config.devices.confirm_rename_new"
)}
</th>
</tr>
${dialogRenames}
</table>
</div>
</ha-expansion-panel>
${dialogNoRenames.length
? html`<br /><br />${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
{ deviceSlug: oldDeviceSlug }
)}:
${dialogNoRenames}`
: nothing}`,
? html`<ha-expansion-panel outlined>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
{
count: dialogNoRenames.length,
deviceSlug: oldDeviceSlug,
}
)}</span
>
${dialogNoRenames}</ha-expansion-panel
>`
: nothing} `,
confirmText: this.hass.localize("ui.common.rename"),
dismissText: this.hass.localize("ui.common.no"),
warning: true,
@@ -1387,11 +1420,15 @@ export class HaConfigDevicePage extends LitElement {
title: this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_no_renamable_entity_ids"
),
text: html`${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
{ deviceSlug: oldDeviceSlug }
)}:
${dialogNoRenames}`,
text: html`<ha-expansion-panel outlined>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_wont_rename",
{ deviceSlug: oldDeviceSlug, count: dialogNoRenames.length }
)}</span
>
${dialogNoRenames}
</ha-expansion-panel>`,
});
}
}
@@ -37,6 +37,7 @@ import {
ThreadDataSet,
ThreadRouter,
addThreadDataSet,
getThreadDataSetTLV,
listThreadDataSets,
removeThreadDataSet,
setPreferredBorderAgent,
@@ -168,8 +169,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
(otbr) => otbr.extended_pan_id === network.dataset!.extended_pan_id
));
const canImportKeychain =
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
otbrForNetwork;
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain;
return html`<ha-card>
<div class="card-header">
@@ -208,8 +208,12 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
${network.routers.map((router) => {
const otbr =
this._otbrInfo && this._otbrInfo[router.extended_address];
const showOverflow =
("dataset" in network && router.border_agent_id) || otbr;
const showDefaultRouter = !!network.dataset;
const isDefaultRouter =
showDefaultRouter &&
router.extended_address ===
network.dataset!.preferred_extended_address;
const showOverflow = showDefaultRouter || otbr;
return html`<ha-list-item
class="router"
twoline
@@ -235,9 +239,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
""}
<span slot="secondary">${router.server}</span>
${showOverflow
? html`${network.dataset &&
router.extended_address ===
network.dataset.preferred_extended_address
? html`${isDefaultRouter
? html`<ha-svg-icon
.path=${mdiCellphoneKey}
.title=${this.hass.localize(
@@ -259,13 +261,9 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
${network.dataset && router.border_agent_id
? html`<ha-list-item
.disabled=${router.border_agent_id ===
network.dataset.preferred_border_agent_id}
>
${router.border_agent_id ===
network.dataset.preferred_border_agent_id
${showDefaultRouter
? html`<ha-list-item .disabled=${isDefaultRouter}>
${isDefaultRouter
? this.hass.localize(
"ui.panel.config.thread.default_router"
)
@@ -321,9 +319,13 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
>
</div>`
: ""}
${canImportKeychain
${canImportKeychain &&
network.dataset?.preferred &&
network.routers?.length
? html`<div class="card-actions">
<mwc-button .otbr=${otbrForNetwork} @click=${this._sendCredentials}
<mwc-button
.networkDataset=${network.dataset}
@click=${this._sendCredentials}
>Send credentials to phone</mwc-button
>
</div>`
@@ -331,17 +333,30 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
</ha-card>`;
}
private _sendCredentials(ev) {
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
if (!otbr) {
private async _sendCredentials(ev) {
const dataset = (ev.currentTarget as any).networkDataset as ThreadDataSet;
if (!dataset) {
return;
}
if (
!dataset.preferred_extended_address &&
!dataset.preferred_border_agent_id
) {
showAlertDialog(this, {
title: "Error",
text: this.hass.localize("ui.panel.config.thread.no_preferred_router"),
});
return;
}
this.hass.auth.external!.fireMessage({
type: "thread/store_in_platform_keychain",
payload: {
mac_extended_address: otbr.extended_address,
border_agent_id: otbr.border_agent_id,
active_operational_dataset: otbr.active_dataset_tlvs,
mac_extended_address: dataset.preferred_extended_address,
border_agent_id: dataset.preferred_border_agent_id,
active_operational_dataset: (
await getThreadDataSetTLV(this.hass, dataset.dataset_id)
).tlv,
extended_pan_id: dataset.extended_pan_id,
},
});
}
@@ -467,10 +482,9 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
const network = (ev.currentTarget as any).network as ThreadNetwork;
const router = (ev.currentTarget as any).router as ThreadRouter;
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
const index =
network.dataset && router.border_agent_id
? Number(ev.detail.index)
: Number(ev.detail.index) + 1;
const index = network.dataset
? Number(ev.detail.index)
: Number(ev.detail.index) + 1;
switch (index) {
case 0:
this._setPreferredBorderAgent(network.dataset!, router);
@@ -24,6 +24,7 @@ import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant, Route } from "../../../../../types";
import { formatAsPaddedHex, sortZHAGroups } from "./functions";
import { zhaTabs } from "./zha-config-dashboard";
import { LocalizeFunc } from "../../../../../common/translations/localize";
export interface GroupRowData extends ZHAGroup {
group?: GroupRowData;
@@ -71,38 +72,35 @@ export class ZHAGroupsDashboard extends LitElement {
});
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer<GroupRowData> =>
narrow
? {
name: {
title: "Group",
sortable: true,
filterable: true,
direction: "asc",
flex: 2,
},
}
: {
name: {
title: this.hass.localize("ui.panel.config.zha.groups.groups"),
sortable: true,
filterable: true,
direction: "asc",
flex: 2,
},
group_id: {
title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
type: "numeric",
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
sortable: true,
},
members: {
title: this.hass.localize("ui.panel.config.zha.groups.members"),
type: "numeric",
template: (group) => html` ${group.members.length} `,
sortable: true,
},
}
(localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<GroupRowData> = {
name: {
title: localize("ui.panel.config.zha.groups.groups"),
sortable: true,
filterable: true,
showNarrow: true,
main: true,
hideable: false,
moveable: false,
direction: "asc",
flex: 2,
},
group_id: {
title: localize("ui.panel.config.zha.groups.group_id"),
type: "numeric",
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
sortable: true,
},
members: {
title: localize("ui.panel.config.zha.groups.members"),
type: "numeric",
template: (group) => html` ${group.members.length} `,
sortable: true,
},
};
return columns;
}
);
protected render(): TemplateResult {
@@ -112,7 +110,7 @@ export class ZHAGroupsDashboard extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.columns=${this._columns(this.narrow)}
.columns=${this._columns(this.hass.localize)}
.data=${this._formattedGroups(this._groups)}
@row-click=${this._handleRowClicked}
clickable
@@ -14,6 +14,11 @@ import "../../../components/ha-circular-progress";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-formfield";
import "../../../components/ha-icon-button";
import "../../../components/ha-password-field";
import "../../../components/ha-radio";
import type { HaRadio } from "../../../components/ha-radio";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
AccessPoints,
@@ -29,10 +34,6 @@ import {
} from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
import { showIPDetailDialog } from "./show-ip-detail-dialog";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import "../../../components/ha-radio";
import type { HaRadio } from "../../../components/ha-radio";
const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -214,8 +215,7 @@ export class HassioNetwork extends LitElement {
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<ha-textfield
type="password"
<ha-password-field
id="psk"
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.wifi_password"
@@ -223,7 +223,7 @@ export class HassioNetwork extends LitElement {
.version=${"wifi"}
@change=${this._handleInputValueChangedWifi}
>
</ha-textfield>
</ha-password-field>
`
: ""}
`

Some files were not shown because too many files have changed in this diff Show More