Compare commits

..

137 Commits

Author SHA1 Message Date
Paul Bottein
58e968cf62 Simplify fully closed or fully open check 2024-01-04 18:32:48 +01:00
Bram Kragten
fe5431976d Bumped version to 20240104.0 2024-01-04 17:43:37 +01:00
Paul Bottein
72dedc7596 Fix thermostat and humidifier card rendering when off (#19281)
* Fix thermostat and humidifier card rendering when off

* Fix action color
2024-01-04 16:18:16 +01:00
Bram Kragten
8615b799c6 Fix due date when no time in certain timezones (#19280)
* Fix due date when no time in certain timezones

* Update dialog-todo-item-editor.ts
2024-01-04 14:24:33 +01:00
Bram Kragten
fd6cae48b7 Remove overflow hidden from profile (#19279) 2024-01-04 13:20:35 +01:00
Bram Kragten
ecadc06b45 fix valve entities row (#19278) 2024-01-04 11:52:38 +01:00
Franck Nijhof
e5d31d85dd Update getStates to support valves (#19277) 2024-01-04 10:54:20 +01:00
Bram Kragten
e427ffca5f Fix turning valve on/off (#19269) 2024-01-03 21:27:30 -05:00
Bram Kragten
4f05bd9e22 Fix circular progress size + fix bug in assist pipeline debug (#19268) 2024-01-03 21:21:42 -05:00
renovate[bot]
0d8c0ac1a0 Update dependency marked to v11.1.1 (#19254) 2024-01-03 17:45:46 -05:00
karwosts
3984702d3e Fix select view dialog (#19267)
* Fix select view dialog

* add import
2024-01-03 20:36:59 +01:00
Bram Kragten
979085a9de Fix supervisor dev build (#19266) 2024-01-03 20:35:54 +01:00
Bram Kragten
7a6fc573c2 Fix version bump script (#19264) 2024-01-03 14:19:30 +00:00
Bram Kragten
6929a9c603 Bumped version to 20240103.0 2024-01-03 14:59:45 +01:00
karwosts
41bb677ebe Remove tile pointer/ripple/index when it has no action (#19137)
* Remove tile pointer/ripple/index when it has no action

* update

* Apply suggestions from code review

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-01-03 14:58:59 +01:00
Bram Kragten
8368fb4408 Fix checking todo item that dont support due date (#19262)
* Fix checking todo item that dont support due date

* make cleaner

* Revert "make cleaner"

This reverts commit fa33b33614.

* Update dialog-todo-item-editor.ts

* do check in 1 place
2024-01-03 13:58:36 +00:00
Paul Bottein
acbb7e0453 Display edit button for climate fan mode feature (#19259) 2024-01-03 14:26:40 +01:00
Bram Kragten
6224cb6994 Bumped version to 20240103.1 2024-01-03 12:28:46 +01:00
Bram Kragten
29fefa1d60 Revert conditional rendering of condition (#19257)
* Fix conditionally showing `triggered by`

* revert conditional rendering

* Update add-automation-element-dialog.ts

* Update add-automation-element-dialog.ts
2024-01-03 11:20:11 +00:00
Bram Kragten
c7a98fa5a1 Bumped version to 20240103.0 2024-01-03 10:37:59 +01:00
Bram Kragten
46565715c9 Calculate used domains on open of action dialog (#19255) 2024-01-03 10:37:30 +01:00
Simon Lamon
8a8b04ed00 Set default values for required and disabled for labeled slider (#19246)
Set default values
2024-01-02 20:00:19 +01:00
Bram Kragten
e721481757 Move notification services to main list (#19235) 2024-01-02 19:58:37 +01:00
renovate[bot]
2933d642ed Update dependency @bundle-stats/plugin-webpack-filter to v4.8.4 (#19230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 13:55:44 -05:00
Bram Kragten
7d412a9913 Bumped version to 20240102.0 2024-01-02 18:49:31 +01:00
Bram Kragten
5eaa6dafd9 Give todo and calendar edit static header (#19233) 2024-01-02 18:48:34 +01:00
Bram Kragten
f994b378f1 Update add-automation-element-dialog.ts 2024-01-02 18:47:39 +01:00
JLo
7d6dfaad15 New copy for device trigger in automation editor (#19232)
New copy for device trigger in automation editor: 
"Set of conditions provided by your device. Great way to start."
2024-01-02 17:39:55 +00:00
Josh McCarty
def2eadc24 Add missing device classes for entity-registry-settings-editor (#19231)
* Add connectivity device class for binary sensors

* Add update device class

* Separate connectivity and update
2024-01-02 17:35:08 +00:00
Bram Kragten
53a25dde26 Change format of service description (#19229) 2024-01-02 18:31:18 +01:00
Bram Kragten
6f7da16d1d Use brand icons in actions (#19227) 2024-01-02 18:11:34 +01:00
Bram Kragten
5ac0360494 Remove references to "service call" from actions (#19226) 2024-01-02 16:08:26 +01:00
karwosts
1b6def4875 Localize a device action string (#19203) 2024-01-02 14:07:34 +01:00
Bram Kragten
52fcf64cfd Automation editor tweaks (#19225)
* Automation editor tweaks

* fix styling
2024-01-02 13:36:06 +01:00
JLo
c650e23432 Review on automation editor text (#19223)
- Added `.` to bloc descriptions
- Changed "Other" into "OTher triggers" "Other conditions" and "Other actions"
- Adapted a few descriptions
2024-01-02 11:08:05 +00:00
renovate[bot]
428604d91d Update babel monorepo to v7.23.7 (#19220) 2024-01-01 20:15:34 -05:00
Bram Kragten
d0c1481f76 Bumped version to 20240101.0 2024-01-01 13:58:47 +01:00
renovate[bot]
6cc9a99b77 Update dependency terser-webpack-plugin to v5.3.10 (#19211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-31 13:47:55 -05:00
renovate[bot]
d25f49b694 Update dependency chai to v5 (#19208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-31 13:46:23 -05:00
renovate[bot]
b92d25e28a Update Material Design Icons to v7.4.47 (#19192) 2023-12-30 21:08:13 -05:00
renovate[bot]
bf5c5bc46f Update dependency open to v10.0.2 (#19206) 2023-12-30 21:06:30 -05:00
renovate[bot]
e7cc842be4 Update dependency rollup-plugin-visualizer to v5.12.0 (#19204) 2023-12-30 21:05:23 -05:00
Bram Kragten
cb29d35949 Allow to clear due date (#19201) 2023-12-30 20:21:33 +01:00
Bram Kragten
fe18f70e51 Only make count field template (#19198) 2023-12-30 18:40:01 +01:00
Bram Kragten
ee57f26415 Sort domains correctly, scroll to top on back (#19197) 2023-12-30 18:39:21 +01:00
karwosts
721ec8e559 Localize some strings in integration panel (#19200) 2023-12-30 18:37:50 +01:00
Erik Montnemery
c584f83071 Fix display of variables tab when automation step not executed (#19186)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-12-30 17:16:54 +00:00
Erik Montnemery
3c2fed5041 Improve message when an automation step was not executed (#19185)
* Improve message when an automation step was not executed

* Use step instead of node
2023-12-30 18:08:24 +01:00
Erik Montnemery
7e6d974438 Fix iteration index when displaying changed variables (#19184) 2023-12-30 14:08:01 +01:00
Bram Kragten
c95a30c837 Use username in example on empty automation page (#19189) 2023-12-29 20:45:51 +01:00
Bram Kragten
cb568d005e dont allow UI editor when templates are used in repeat (#19188) 2023-12-29 17:59:11 +00:00
Bram Kragten
442cce1574 Fix play media action not shown (#19187) 2023-12-29 18:56:15 +01:00
Erik Montnemery
c25baf25ad Exclude hidden entities from scenes (#19179) 2023-12-29 18:41:10 +01:00
renovate[bot]
5e279405c7 Update typescript-eslint monorepo to v6.16.0 (#19174) 2023-12-28 20:32:43 -05:00
Bram Kragten
e5707b423f Bumped version to 20231228.0 2023-12-28 15:33:39 +01:00
Bram Kragten
7983556f98 Update sortable styling (#19169) 2023-12-28 15:32:49 +01:00
Bram Kragten
1916dff57b Keep todo item selected when checked/unchecked (#19168)
* Keep todo item selected when checked/unchecked

* update label when in reoder mode
2023-12-28 15:32:40 +01:00
Bram Kragten
b11563d618 Small automation editor fixes (#19160) 2023-12-28 15:32:31 +01:00
Bram Kragten
b180a587bf Address feedback of automation editor (#19167) 2023-12-28 14:18:18 +01:00
Simon Lamon
5b11e0ce29 Fix self referencing translation (#19159) 2023-12-27 20:26:17 +01:00
Bram Kragten
d6684c5806 Revert hide automation desc in advanced mode (#19158) 2023-12-27 17:46:17 +01:00
Bram Kragten
1a0e3890f4 Bumped version to 20231227.0 2023-12-27 17:22:34 +01:00
Bram Kragten
caece9d6ad Add QR code element (#19155)
* Add QR code element

* fixes

* move to workflow

* limit webpack imports
2023-12-27 17:22:09 +01:00
renovate[bot]
953a3793c4 Update dependency vue to v2.7.16 (#19156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 17:19:27 +01:00
Jan-Philipp Benecke
8bfae3b4cf Fix ha-combo-box helper text not displaying text (#19154)
* Fix ha-combo-box helper text

* Update src/components/ha-combo-box.ts

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-12-27 15:22:19 +00:00
Bram Kragten
6911685bd0 Only focus search when not in fullscreen dialog (#19153) 2023-12-27 12:45:49 +00:00
Bram Kragten
71025eaf4d Fix dialog headers (#19152)
* Fix dialog headers

* increase padding
2023-12-27 13:45:36 +01:00
Bram Kragten
3aa612b766 Todo list layout fixes (#19150) 2023-12-27 13:22:36 +01:00
Simon Lamon
a2ffd0ae83 Circular progress improvement in update area (#18983)
* Fixes

* Add aria label again
2023-12-27 11:07:40 +01:00
Simon Lamon
c399da586e Increase battery column width (#19143) 2023-12-27 11:00:48 +01:00
Erik Montnemery
d7826e4e6c Show +/- volume buttons for media players which support VOLUME_SET (#19111)
* Show +/- volume buttons for media players which support VOLUME_SET

* Fixup
2023-12-27 11:00:12 +01:00
Yosi Levy
f5d13c9079 Various RTL fixes (#19134)
* Various RTL fixes

* Various RTL fixes
2023-12-27 10:59:28 +01:00
Paul Bottein
01a142790f Add update actions card feature (#19110)
* Add update tile feature

* Fix translations

* Add confirmation dialog

* Remove unused styles

* Fix gallery

* Update wording

* Update src/translations/en.json
2023-12-27 10:55:14 +01:00
karwosts
df54687de1 Fix trailing energy gaps, refactor chart options (#19117)
* Fix trailing gap on energy graph cards

* Use date methods instead of unix time calculation

* Fix trailing energy gaps, refactor chart options

---------

Co-authored-by: Till Fleisch <till@fleisch.dev>
2023-12-27 10:53:16 +01:00
karwosts
bded31b311 Fix entities sort for hidden/readonly (#19124) 2023-12-27 10:51:36 +01:00
c0ffeeca7
67e573aff7 HVAC in demo - fix typo (#19132) 2023-12-27 10:25:30 +01:00
c0ffeeca7
6295c4ac76 Add Oxford comma (#19133) 2023-12-27 10:24:57 +01:00
renovate[bot]
469811847f Lock file maintenance (#19084)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-26 11:53:07 -05:00
renovate[bot]
8a1aefefca Update vaadinWebComponents monorepo to v24.3.2 (#19141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-25 15:39:49 -05:00
renovate[bot]
bea16028a1 Update dependency open to v10.0.1 (#19145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-25 15:38:06 -05:00
dependabot[bot]
bedb7d1d9e Bump actions/setup-node from 4.0.0 to 4.0.1 (#19140)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-25 15:37:25 -05:00
renovate[bot]
1a3a20f478 Update dependency element-internals-polyfill to v1.3.10 (#19136) 2023-12-24 23:25:00 -05:00
Bram Kragten
68ecb7c219 Store height and width of dialog, autofocus searchbar (#19122) 2023-12-23 00:03:52 +01:00
renovate[bot]
61b04a882b Update dependency core-js to v3.34.0 (#18974)
* Update dependency core-js to v3.34.0

* Update Babel setting with package.json version

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-12-22 15:41:33 -05:00
renovate[bot]
f8e621c5b9 Update dependency open to v10 (#19116)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-22 14:16:41 -05:00
Bram Kragten
2f2209682e Update learn more URLs to just "Learn more" 2023-12-22 16:40:20 +01:00
Bram Kragten
f4f361b51a Add automation picker descriptions (#19121) 2023-12-22 16:35:41 +01:00
Erik Montnemery
55e59f8cb0 Add valve to switch_as_x domains (#19118) 2023-12-22 11:34:09 +01:00
renovate[bot]
6d20ed0a22 Update dependency hls.js to v1.4.14 (#19112) 2023-12-21 20:22:36 -05:00
renovate[bot]
874f604295 Update typescript-eslint monorepo to v6.15.0 (#19113) 2023-12-21 20:21:23 -05:00
renovate[bot]
30d36a11c1 Update dependency eslint-plugin-lit to v1.11.0 (#19103) 2023-12-21 20:19:36 -05:00
Bram Kragten
09dcc29175 Add description and due support to todo lists (#19107) 2023-12-21 21:30:24 +01:00
Bram Kragten
8f07e6f141 Group add automation elements in dialog (#19086)
* Group add automation elements in dialog

* Add search

* clear filter on close

* Split out services

* group services by integration type

* Update add-automation-element-dialog.ts

* fix typing

* clear filter on back

* Update add-automation-element-dialog.ts

* Fix search

* scroll to top

* Add service descriptions

* fix clipboard

* Move play media, sort services

* use helpers

* move to data

* Move building blocks to a group

* fix search

* Update add-automation-element-dialog.ts

* Update en.json

* fix alignment of single line and multi line items

* use repeat instead of map
2023-12-21 21:01:27 +01:00
Bram Kragten
7b6b5724e1 Add descriptions to automation page sections (#19081)
* Add descriptions to automation page sections

* add name key

* Update manual-automation-editor.ts

* hide explanation if there is content

* Update src/translations/en.json

* hide when advanced
2023-12-21 21:01:11 +01:00
Quentame
521c0b58c8 Add climate fan mode feature to thermostat & tile cards (#19094)
* climate: Add fan_only exemple

* climate: add fan_mode feature to thermostat & tile cards

* review: update dropdown icon
2023-12-21 15:49:08 +01:00
Bram Kragten
53839ab7b1 Update text on empty automation/script/scene pages (#19095) 2023-12-21 15:36:56 +01:00
Chris Roberts
dcfe9617b3 Make disabled sliders properly visible (#19102)
Followed similar variable usage by ha-*-chip components.

Fixes #19101
2023-12-20 21:26:48 +00:00
Matthias Alphart
58eebf2dbd Clear Area picker when "Add new area" was canceled (#19088) 2023-12-20 22:17:31 +01:00
Paul Bottein
af9b64c6f0 Add option to show current temperature on thermostat card (#19049)
* Fix unit position when no decimal

* Add option to switch between current and target for thermostat card

* Refactor code

* Clean label code

* Rename config name
2023-12-20 14:41:22 +01:00
Paul Bottein
2b18ac8d4e Add option to show current humidity on humidifier card (#19079) 2023-12-20 14:12:59 +01:00
karwosts
2306234063 Localize date-range-picker (#18945) 2023-12-19 09:25:56 +01:00
renovate[bot]
883a9e422e Update dependency eslint to v8.56.0 (#19083) 2023-12-18 21:24:47 -05:00
Dmitry Tsydzik
d77ce721e3 Add possibility to define group id manually for ZHA (#18932)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-12-18 15:37:57 +01:00
Bram Kragten
325ad6f721 Fix step any in number selector (#19077) 2023-12-18 15:24:46 +01:00
Simon Lamon
d762a9365f Automation traces localization (#18862) 2023-12-18 15:06:41 +01:00
Paul Bottein
ce983f043e Fix empty classmap for thermostat card (#19078) 2023-12-18 14:00:37 +01:00
Bram Kragten
45b7ebbe46 Add empty page to automation/script/scene config (#19075) 2023-12-18 13:59:36 +01:00
Paul Bottein
3e7fa66790 Add valve entity (#19024)
* Add valve entity

* Update icon based on device class

* Check assumed state first

* Reset mode if entity id changes
2023-12-18 13:53:52 +01:00
renovate[bot]
ad543dbffb Update dependency webpackbar to v6 (#19076)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-18 12:31:13 +00:00
karwosts
cdd2c7be9a Fix date-range-picker overflow on energy dashboard small screen (#19026) 2023-12-18 13:20:21 +01:00
Simon Lamon
ddf6945190 Localize state condition in automation editor (#18864)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-12-18 13:19:19 +01:00
Simon Lamon
f2745747ba Replace more polymer paper inputs and remove paper-input style attributes (#18929) 2023-12-18 13:05:56 +01:00
Charles Garwood
ff9d179c13 Minor tweak to Z-Wave JS Config Dashboard layout (#19032) 2023-12-18 12:56:30 +01:00
karwosts
e813108c66 Optional boolean service field defaults to false (#19043) 2023-12-18 12:56:01 +01:00
karwosts
62d8cdfcf9 Fix missing tooltips in energy-usage-graph (#19031) 2023-12-18 12:52:52 +01:00
karwosts
5b9d46e350 Add a safe mode item to the github issue template checkist (#19022) 2023-12-18 12:49:03 +01:00
renovate[bot]
e5db95b2d2 Update vaadinWebComponents monorepo to v24.3.1 (#19052)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-18 12:08:21 +01:00
karwosts
a8f7c7c999 Fix hide legend toggle in stats graph editor (#19021) 2023-12-18 11:55:50 +01:00
c0ffeeca7
8a423fb775 Assist, no wake word: point docs link to wake word installation proce… (#19040)
Assist, no wake word: point docs link to wake word installation procedure
2023-12-18 11:47:32 +01:00
c0ffeeca7
90c3d69af6 No supervisor message: fix typo (#19041) 2023-12-18 11:46:44 +01:00
renovate[bot]
bcb2d73c5c Update dependency @material/web to v1.1.1 (#19064)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-18 11:44:20 +01:00
dependabot[bot]
8a6cea12e1 Bump github/codeql-action from 2 to 3 (#19072)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 09:23:52 +01:00
renovate[bot]
0056de146a Update dependency @codemirror/view to v6.22.3 (#19063)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-17 19:04:01 -05:00
renovate[bot]
0bcdbef11e Update dependency eslint-plugin-import to v2.29.1 (#19070)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-17 19:02:58 -05:00
renovate[bot]
1b208cb531 Update dependency marked to v11.1.0 (#19054) 2023-12-15 21:12:42 -05:00
renovate[bot]
6a975e260c Update dependency @rollup/plugin-json to v6.1.0 (#19057) 2023-12-15 21:10:09 -05:00
renovate[bot]
02d2bde269 Update dependency @babel/helper-define-polyfill-provider to v0.4.4 (#19047) 2023-12-15 01:52:14 +00:00
renovate[bot]
70cfa7f48a Update babel monorepo to v7.23.6 (#19046) 2023-12-14 20:41:13 -05:00
renovate[bot]
b7d4b9c21b Update typescript-eslint monorepo to v6.14.0 (#19051) 2023-12-14 20:35:42 -05:00
renovate[bot]
673c947c11 Update dependency lit-analyzer to v2.0.2 (#19053) 2023-12-14 20:34:40 -05:00
Arno
2682a55148 Fixing issue #10581 Automation Debug Trace Graph not displaying right in Safari (#19036) 2023-12-14 11:53:53 +01:00
renovate[bot]
616f6ddf5f Update dependency @koa/cors to v5 [SECURITY] (#19018) 2023-12-14 02:38:00 +00:00
renovate[bot]
29769813bc Update dependency @codemirror/view to v6.22.2 (#19016) 2023-12-14 01:58:39 +00:00
renovate[bot]
221c46344a Update dependency rollup-plugin-visualizer to v5.11.0 (#19025) 2023-12-13 20:46:12 -05:00
renovate[bot]
a33eb25c92 Update dependency prettier to v3.1.1 (#19027) 2023-12-13 20:45:00 -05:00
Simon Lamon
1dc61320a6 Fix Github Action labeler configuration (#19023) 2023-12-12 20:35:21 +01:00
karwosts
ad556a43f9 Sort default dashboard area cards by alphabetical order if no order specified (#18989) 2023-12-12 14:25:12 +01:00
169 changed files with 6935 additions and 3254 deletions

View File

@@ -24,6 +24,7 @@ body:
required: true
- label: I have tried a different browser to see if it is related to my browser.
required: true
- label: I have tried reproducing the issue in [safe mode](https://www.home-assistant.io/blog/2023/11/01/release-202311/#restarting-into-safe-mode) to rule out problems with unsupported custom resources.
- type: markdown
attributes:
value: |

50
.github/labeler.yml vendored
View File

@@ -1,31 +1,45 @@
Build:
- build-scripts/**
- .browserslistrc
- gulpfile.js
- changed-files:
- any-glob-to-any-file:
- build-scripts/**
- .browserslistrc
- gulpfile.js
Cast:
- cast/src/**
- src/cast/**
- changed-files:
- any-glob-to-any-file:
- cast/src/**
- src/cast/**
Demo:
- demo/src/**
- src/fake_data/**
- changed-files:
- any-glob-to-any-file:
- demo/src/**
- src/fake_data/**
Design:
- gallery/src/**
- src/fake_data/**
- changed-files:
- any-glob-to-any-file:
- gallery/src/**
- src/fake_data/**
Dependencies:
- package.json
- renovate.json
- yarn.lock
- .yarn/**
- .yarnrc.yml
- .nvmrc
- changed-files:
- any-glob-to-any-file:
- package.json
- renovate.json
- yarn.lock
- .yarn/**
- .yarnrc.yml
- .nvmrc
GitHub Actions:
- .github/workflows/**
- .github/*.yml
- changed-files:
- any-glob-to-any-file:
- .github/workflows/**
- .github/*.yml
Supervisor:
- hassio/src/**
- changed-files:
- any-glob-to-any-file:
- hassio/src/**

View File

@@ -26,7 +26,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -62,7 +62,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,14 +57,14 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Build resources
run: ./node_modules/.bin/gulp build-translations build-locale-data
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data
- name: Run Tests
run: yarn run test
build:
@@ -75,7 +75,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -99,7 +99,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3

View File

@@ -27,7 +27,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -63,7 +63,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -1,6 +1,7 @@
const path = require("path");
const env = require("./env.cjs");
const paths = require("./paths.cjs");
const { dependencies } = require("../package.json");
// GitHub base URL to use for production source maps
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
@@ -90,7 +91,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
"@babel/preset-env",
{
useBuiltIns: latestBuild ? false : "usage",
corejs: latestBuild ? false : "3.33",
corejs: latestBuild ? false : dependencies["core-js"],
bugfixes: true,
shippedProposals: true,
},
@@ -140,7 +141,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
// Import helpers and regenerator from runtime package
[
"@babel/plugin-transform-runtime",
{ version: require("../package.json").dependencies["@babel/runtime"] },
{ version: dependencies["@babel/runtime"] },
],
// Support some proposals still in TC39 process
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],

View File

@@ -426,6 +426,7 @@ gulp.task(
"fetch-nightly-translations",
gulp.series("clean-translations", "ensure-translations-build-dir")
),
gulp.parallel("create-test-metadata", "create-test-translation"),
"build-master-translation",
"build-merged-translations",
"build-translation-fragment-supervisor",

View File

@@ -509,7 +509,7 @@ export default {
away_mode: "on",
aux_heat: "off",
unit_of_measurement: "°C",
friendly_name: "Hvac",
friendly_name: "HVAC",
supported_features: 3833,
},
last_changed: "2018-07-19T10:44:46.200650+00:00",

View File

@@ -35,6 +35,18 @@ const ENTITIES = [
friendly_name: "Nest",
supported_features: 43,
}),
getEntity("climate", "sensibo", "fan_only", {
current_temperature: null,
temperature: null,
min_temp: 0,
max_temp: 1,
target_temp_step: 1,
hvac_modes: ["fan_only", "off"],
friendly_name: "Sensibo purifier",
fan_modes: ["low", "high"],
fan_mode: "low",
supported_features: 9,
}),
getEntity("climate", "unavailable", "unavailable", {
supported_features: 43,
}),
@@ -57,6 +69,23 @@ const CONFIGS = [
entity: climate.nest
`,
},
{
heading: "Fan only example",
config: `
- type: thermostat
entity: climate.sensibo
features:
- type: climate-hvac-modes
hvac_modes:
- fan_only
- 'off'
- type: climate-fan-modes
style: icons
fan_modes:
- low
- high
`,
},
{
heading: "Unavailable",
config: `

View File

@@ -31,6 +31,21 @@ const ENTITIES = [
max_temp: 30,
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
}),
getEntity("climate", "fan", "fan_only", {
friendly_name: "Basic fan",
hvac_modes: ["fan_only", "off"],
hvac_mode: "fan_only",
fan_modes: ["low", "high"],
fan_mode: "low",
current_temperature: null,
temperature: null,
min_temp: 0,
max_temp: 1,
target_temp_step: 1,
supported_features:
// eslint-disable-next-line no-bitwise
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE,
}),
getEntity("climate", "hvac", "auto", {
friendly_name: "Basic hvac",
hvac_modes: ["auto", "off"],

View File

@@ -1,12 +1,6 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import {
UPDATE_SUPPORT_BACKUP,
UPDATE_SUPPORT_PROGRESS,
UPDATE_SUPPORT_INSTALL,
UPDATE_SUPPORT_RELEASE_NOTES,
} from "../../../../src/data/update";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import {
@@ -15,13 +9,14 @@ import {
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
import { LONG_TEXT } from "../../data/text";
import { UpdateEntityFeature } from "../../../../src/data/update";
const base_attributes = {
title: "Awesome",
installed_version: "1.2.2",
latest_version: "1.2.3",
release_url: "https://home-assistant.io",
supported_features: UPDATE_SUPPORT_INSTALL,
supported_features: UpdateEntityFeature.INSTALL,
skipped_version: null,
in_progress: false,
release_summary:
@@ -61,7 +56,7 @@ const ENTITIES = [
getEntity("update", "update7", "on", {
...base_attributes,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_BACKUP,
base_attributes.supported_features + UpdateEntityFeature.BACKUP,
friendly_name: "With backup support",
}),
getEntity("update", "update8", "on", {
@@ -73,21 +68,21 @@ const ENTITIES = [
...base_attributes,
in_progress: 25,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
friendly_name: "With 25 in_progress",
}),
getEntity("update", "update10", "on", {
...base_attributes,
in_progress: 50,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
friendly_name: "With 50 in_progress",
}),
getEntity("update", "update11", "on", {
...base_attributes,
in_progress: 75,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
friendly_name: "With 75 in_progress",
}),
getEntity("update", "update12", "unavailable", {
@@ -114,19 +109,19 @@ const ENTITIES = [
...base_attributes,
friendly_name: "Update with release notes",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES,
}),
getEntity("update", "update17", "off", {
...base_attributes,
friendly_name: "Update with release notes error",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES,
}),
getEntity("update", "update18", "off", {
...base_attributes,
friendly_name: "Update with release notes loading",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES,
}),
getEntity("update", "update19", "on", {
...base_attributes,
@@ -142,9 +137,10 @@ const ENTITIES = [
getEntity("update", "update21", "on", {
...base_attributes,
in_progress: true,
friendly_name: "Update with in_progress true and UPDATE_SUPPORT_PROGRESS",
friendly_name:
"Update with in_progress true and UpdateEntityFeature.PROGRESS",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
}),
];

View File

@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-select";
import {
extractApiErrorMessage,

View File

@@ -25,7 +25,7 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.23.5",
"@babel/runtime": "7.23.7",
"@braintree/sanitize-url": "7.0.0",
"@codemirror/autocomplete": "6.11.1",
"@codemirror/commands": "6.3.2",
@@ -33,7 +33,7 @@
"@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.5",
"@codemirror/state": "6.3.3",
"@codemirror/view": "6.22.1",
"@codemirror/view": "6.22.3",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.0",
"@formatjs/intl-displaynames": "6.6.4",
@@ -80,9 +80,9 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.1",
"@mdi/js": "7.3.67",
"@mdi/svg": "7.3.67",
"@material/web": "=1.1.1",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@polymer/paper-input": "3.2.1",
"@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1",
@@ -90,8 +90,8 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.2.5",
"@vaadin/vaadin-themable-mixin": "24.2.5",
"@vaadin/combo-box": "24.3.2",
"@vaadin/vaadin-themable-mixin": "24.3.2",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -101,16 +101,16 @@
"app-datepicker": "5.1.1",
"chart.js": "4.4.1",
"comlink": "4.4.1",
"core-js": "3.33.3",
"core-js": "3.34.0",
"cropperjs": "1.6.1",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"element-internals-polyfill": "1.3.9",
"element-internals-polyfill": "1.3.10",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
"hls.js": "1.4.13",
"hls.js": "1.4.14",
"home-assistant-js-websocket": "9.1.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.8",
@@ -119,7 +119,7 @@
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.4.4",
"marked": "11.0.1",
"marked": "11.1.1",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@@ -138,7 +138,7 @@
"unfetch": "5.0.0",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
"vue": "2.7.15",
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
"workbox-cacheable-response": "7.0.0",
@@ -150,14 +150,14 @@
"xss": "1.0.14"
},
"devDependencies": {
"@babel/core": "7.23.5",
"@babel/helper-define-polyfill-provider": "0.4.3",
"@babel/plugin-proposal-decorators": "7.23.5",
"@babel/plugin-transform-runtime": "7.23.4",
"@babel/preset-env": "7.23.5",
"@babel/core": "7.23.7",
"@babel/helper-define-polyfill-provider": "0.4.4",
"@babel/plugin-proposal-decorators": "7.23.7",
"@babel/plugin-transform-runtime": "7.23.7",
"@babel/preset-env": "7.23.7",
"@babel/preset-typescript": "7.23.3",
"@bundle-stats/plugin-webpack-filter": "4.8.3",
"@koa/cors": "4.0.0",
"@bundle-stats/plugin-webpack-filter": "4.8.4",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.1.0",
"@octokit/auth-oauth-device": "6.0.1",
"@octokit/plugin-retry": "6.0.1",
@@ -165,7 +165,7 @@
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "25.0.7",
"@rollup/plugin-json": "6.0.1",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -184,22 +184,22 @@
"@types/tar": "6.1.10",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.13.2",
"@typescript-eslint/parser": "6.13.2",
"@typescript-eslint/eslint-plugin": "6.16.0",
"@typescript-eslint/parser": "6.16.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.10",
"chai": "5.0.0",
"del": "7.1.0",
"eslint": "8.55.0",
"eslint": "8.56.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.29.0",
"eslint-plugin-lit": "1.10.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.11.0",
"eslint-plugin-lit-a11y": "4.1.1",
"eslint-plugin-unused-imports": "3.0.0",
"eslint-plugin-wc": "2.0.4",
@@ -217,25 +217,25 @@
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "15.2.0",
"lit-analyzer": "2.0.1",
"lit-analyzer": "2.0.2",
"lodash.template": "4.5.0",
"magic-string": "0.30.5",
"map-stream": "0.0.7",
"mocha": "10.2.0",
"object-hash": "3.0.0",
"open": "9.1.0",
"open": "10.0.2",
"pinst": "3.0.0",
"prettier": "3.1.0",
"prettier": "3.1.1",
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.10.0",
"rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.5",
"sinon": "17.0.1",
"source-map-url": "0.4.1",
"systemjs": "6.14.2",
"tar": "6.2.0",
"terser-webpack-plugin": "5.3.9",
"terser-webpack-plugin": "5.3.10",
"ts-lit-plugin": "2.0.1",
"typescript": "5.3.3",
"vinyl-buffer": "1.0.1",
@@ -245,7 +245,7 @@
"webpack-dev-server": "4.15.1",
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "5.0.2",
"webpackbar": "6.0.0",
"workbox-build": "7.0.0"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20231208.2"
version = "20240104.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@@ -1,7 +1,10 @@
#!/usr/bin/env node
const fs = require("fs");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
/* eslint-disable no-console */
import fs from "fs";
import util from "util";
import child_process from "child_process";
const exec = util.promisify(child_process.exec);
function patch(version) {
const parts = version.split(".");
@@ -18,7 +21,7 @@ function today() {
function auto(version) {
const todayVersion = today();
if (todayVersion !== version) {
if (todayVersion.split(".")[0] !== version.split(".")[0]) {
return todayVersion;
}
return patch(version);
@@ -44,7 +47,7 @@ async function main(args) {
commit = true;
} else {
method = args.length > 0 && methods[args[0]];
commit = args.length > 1 && args[1] == "--commit";
commit = args.length > 1 && args[1] === "--commit";
}
if (!method) {

View File

@@ -47,7 +47,7 @@ export class HaAuthTextField extends HaTextField {
// TODO: live() directive needs casting for lit-analyzer
// https://github.com/runem/lit-analyzer/pull/91/files
// TODO: lit-analyzer labels min/max as (number|string) instead of string
return html` <input
return html`<input
aria-labelledby=${ifDefined(ariaLabelledbyOrUndef)}
aria-controls=${ifDefined(ariaControlsOrUndef)}
aria-describedby=${ifDefined(ariaDescribedbyOrUndef)}

View File

@@ -29,6 +29,7 @@ import {
mdiFlash,
mdiFlower,
mdiFormatListBulleted,
mdiFormatListCheckbox,
mdiFormTextbox,
mdiGauge,
mdiGoogleAssistant,
@@ -64,6 +65,7 @@ import {
mdiTransmissionTower,
mdiWater,
mdiWaterPercent,
mdiWeatherPartlyCloudy,
mdiWeatherPouring,
mdiWeatherRainy,
mdiWeatherWindy,
@@ -128,6 +130,7 @@ export const FIXED_DOMAIN_ICONS = {
updater: mdiCloudUpload,
vacuum: mdiRobotVacuum,
wake_word: mdiChatSleep,
weather: mdiWeatherPartlyCloudy,
zone: mdiMapMarkerRadius,
};
@@ -166,6 +169,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
precipitation_intensity: mdiWeatherPouring,
pressure: mdiGauge,
reactive_power: mdiFlash,
shopping_List: mdiFormatListCheckbox,
signal_strength: mdiWifi,
sound_pressure: mdiEarHearing,
speed: mdiSpeedometer,
@@ -250,6 +254,7 @@ export const DOMAINS_INPUT_ROW = [
"text",
"time",
"vacuum",
"valve",
];
/** States that we consider "off". */
@@ -268,6 +273,7 @@ export const DOMAINS_TOGGLE = new Set([
"group",
"automation",
"humidifier",
"valve",
]);
/** Domains that have a dynamic entity image / picture. */

View File

@@ -0,0 +1,31 @@
import memoizeOne from "memoize-one";
export const localizeWeekdays = memoizeOne(
(language: string, short: boolean): string[] => {
const days: string[] = [];
const format = new Intl.DateTimeFormat(language, {
weekday: short ? "short" : "long",
timeZone: "UTC",
});
for (let i = 0; i < 7; i++) {
const date = new Date(Date.UTC(1970, 0, 1 + 3 + i));
days.push(format.format(date));
}
return days;
}
);
export const localizeMonths = memoizeOne(
(language: string, short: boolean): string[] => {
const months: string[] = [];
const format = new Intl.DateTimeFormat(language, {
month: short ? "short" : "long",
timeZone: "UTC",
});
for (let i = 0; i < 12; i++) {
const date = new Date(Date.UTC(1970, 0 + i, 1));
months.push(format.format(date));
}
return months;
}
);

View File

@@ -28,10 +28,12 @@ import {
mdiLockAlert,
mdiLockClock,
mdiLockOpen,
mdiMeterGas,
mdiMotionSensor,
mdiPackage,
mdiPackageDown,
mdiPackageUp,
mdiPipeValve,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
@@ -274,6 +276,16 @@ export const domainIconWithoutDefault = (
: mdiPackageUp
: mdiPackage;
case "valve":
switch (stateObj?.attributes.device_class) {
case "water":
return mdiPipeValve;
case "gas":
return mdiMeterGas;
default:
return mdiPipeValve;
}
case "water_heater":
return compareState === "off" ? mdiWaterBoilerOff : mdiWaterBoiler;

View File

@@ -50,6 +50,7 @@ export const FIXED_DOMAIN_STATES = {
timer: ["active", "idle", "paused"],
update: ["on", "off"],
vacuum: ["cleaning", "docked", "error", "idle", "paused", "returning"],
valve: ["closed", "closing", "open", "opening"],
weather: [
"clear-night",
"cloudy",

View File

@@ -42,6 +42,8 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
return compareState !== "standby";
case "vacuum":
return !["idle", "docked", "paused"].includes(compareState);
case "valve":
return compareState !== "closed";
case "plant":
return compareState === "problem";
case "group":

View File

@@ -37,6 +37,7 @@ const STATE_COLORED_DOMAIN = new Set([
"timer",
"update",
"vacuum",
"valve",
"water_heater",
]);

View File

@@ -101,7 +101,8 @@ export class StatisticsChart extends LitElement {
changedProps.has("unit") ||
changedProps.has("period") ||
changedProps.has("chartType") ||
changedProps.has("logarithmicScale")
changedProps.has("logarithmicScale") ||
changedProps.has("hideLegend")
) {
this._createOptions();
}

View File

@@ -5,6 +5,10 @@ import DateRangePicker from "vue2-daterange-picker";
// @ts-ignore
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
import { fireEvent } from "../common/dom/fire_event";
import {
localizeWeekdays,
localizeMonths,
} from "../common/datetime/localize_date";
// Set the current date to the left picker instead of the right picker because the right is hidden
const CustomDateRangePicker = Vue.extend({
@@ -63,6 +67,10 @@ const Component = Vue.extend({
type: Boolean,
default: false,
},
language: {
type: String,
default: "en",
},
},
render(createElement) {
// @ts-expect-error
@@ -77,6 +85,8 @@ const Component = Vue.extend({
ranges: this.ranges ? {} : false,
"locale-data": {
firstDay: this.firstDay,
daysOfWeek: localizeWeekdays(this.language, true),
monthNames: localizeMonths(this.language, false),
},
},
model: {
@@ -145,6 +155,8 @@ class DateRangePickerElement extends WrappedElement {
);
color: var(--primary-text-color);
min-width: initial !important;
max-height: var(--date-range-picker-max-height);
overflow-y: auto;
}
.daterangepicker:before {
display: none;
@@ -162,7 +174,7 @@ class DateRangePickerElement extends WrappedElement {
color: var(--secondary-text-color);
border-radius: 0;
outline: none;
width: 32px;
min-width: 32px;
height: 32px;
}
.daterangepicker td.off,
@@ -238,6 +250,9 @@ class DateRangePickerElement extends WrappedElement {
}
.daterangepicker .drp-calendar.left {
padding: 8px;
width: unset;
max-width: unset;
min-width: 270px;
}
.daterangepicker.show-calendar .ranges {
margin-top: 0;

View File

@@ -446,6 +446,7 @@ export class HaAreaPicker extends LitElement {
cancel: () => {
this._setValue(undefined);
this._suggestion = undefined;
this.comboBox.setInputValue("");
},
});
}

View File

@@ -86,6 +86,7 @@ export class HaBigNumber extends LitElement {
.value .decimal {
font-size: 0.42em;
line-height: 1.33;
min-height: 1.33em;
}
.value .unit {
font-size: 0.33em;

View File

@@ -3,9 +3,15 @@ import { CheckListItemBase } from "@material/mwc-list/mwc-check-list-item-base";
import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css";
import { styles } from "@material/mwc-list/mwc-list-item.css";
import { customElement } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-check-list-item")
export class HaCheckListItem extends CheckListItemBase {
async onChange(event) {
super.onChange(event);
fireEvent(this, event.type);
}
static override styles = [
styles,
controlStyles,
@@ -22,6 +28,15 @@ export class HaCheckListItem extends CheckListItemBase {
margin-inline-start: 0px;
direction: var(--direction);
}
.mdc-deprecated-list-item__meta {
flex-shrink: 0;
direction: var(--direction);
margin-inline-start: auto;
margin-inline-end: 0;
}
.mdc-deprecated-list-item__graphic {
margin-top: var(--check-list-item-graphic-margin-top);
}
`,
];
}

View File

@@ -180,7 +180,7 @@ export class HaComboBox extends LitElement {
></div>`}
.icon=${this.icon}
.invalid=${this.invalid}
helper=${ifDefined(this.helper)}
.helper=${this.helper}
helperPersistent
>
<slot name="icon" slot="leadingIcon"></slot>

View File

@@ -18,7 +18,8 @@ export interface datePickerDialogParams {
max?: string;
locale?: string;
firstWeekday?: number;
onChange: (value: string) => void;
canClear?: boolean;
onChange: (value: string | undefined) => void;
}
const showDatePickerDialog = (
@@ -49,6 +50,8 @@ export class HaDateInput extends LitElement {
@property() public helper?: string;
@property({ type: Boolean }) public canClear?: boolean;
render() {
return html`<ha-textfield
.label=${this.label}
@@ -58,6 +61,7 @@ export class HaDateInput extends LitElement {
helperPersistent
readonly
@click=${this._openDialog}
@keydown=${this._keyDown}
.value=${this.value
? formatDateNumeric(
new Date(`${this.value.split("T")[0]}T00:00:00`),
@@ -82,13 +86,23 @@ export class HaDateInput extends LitElement {
min: this.min || "1970-01-01",
max: this.max,
value: this.value,
canClear: this.canClear,
onChange: (value) => this._valueChanged(value),
locale: this.locale.language,
firstWeekday: firstWeekdayIndex(this.locale),
});
}
private _valueChanged(value: string) {
private _keyDown(ev: KeyboardEvent) {
if (!this.canClear) {
return;
}
if (["Backspace", "Delete"].includes(ev.key)) {
this._valueChanged(undefined);
}
}
private _valueChanged(value: string | undefined) {
if (this.value !== value) {
this.value = value;
fireEvent(this, "change");

View File

@@ -253,6 +253,7 @@ export class HaDateRangePicker extends LitElement {
opening-direction=${this.openingDirection ||
this._calcedOpeningDirection}
first-day=${firstWeekdayIndex(this.hass.locale)}
language=${this.hass.locale.language}
>
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
${!this.minimal

View File

@@ -50,6 +50,15 @@ export class HaDialogDatePicker extends LitElement {
@datepicker-value-updated=${this._valueChanged}
.firstDayOfWeek=${this._params.firstWeekday}
></app-datepicker>
${this._params.canClear
? html`<mwc-button
slot="secondaryAction"
@click=${this._clear}
class="warning"
>
${this.hass.localize("ui.dialogs.date-picker.clear")}
</mwc-button>`
: nothing}
<mwc-button slot="secondaryAction" @click=${this._setToday}>
${this.hass.localize("ui.dialogs.date-picker.today")}
</mwc-button>
@@ -66,6 +75,11 @@ export class HaDialogDatePicker extends LitElement {
this._value = ev.detail.value;
}
private _clear() {
this._params?.onChange(undefined);
this.closeDialog();
}
private _setToday() {
const today = new Date();
this._value = format(today, "yyyy-MM-dd");

View File

@@ -13,13 +13,15 @@ export const createCloseHeading = (
hass: HomeAssistant | undefined,
title: string | TemplateResult
) => html`
<div class="header_title">${title}</div>
<ha-icon-button
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
.path=${mdiClose}
dialogAction="close"
class="header_button"
></ha-icon-button>
<div class="header_title">
<span>${title}</span>
<ha-icon-button
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
.path=${mdiClose}
dialogAction="close"
class="header_button"
></ha-icon-button>
</div>
`;
@customElement("ha-dialog")
@@ -94,15 +96,12 @@ export class HaDialog extends DialogBase {
}
.mdc-dialog__title {
padding: 24px 24px 0 24px;
text-overflow: ellipsis;
overflow: hidden;
}
.mdc-dialog__actions {
padding: 12px 24px 12px 24px;
}
.mdc-dialog__title::before {
display: block;
height: 0px;
content: unset;
}
.mdc-dialog .mdc-dialog__content {
position: var(--dialog-content-position, relative);
@@ -126,19 +125,26 @@ export class HaDialog extends DialogBase {
flex-direction: column;
}
.header_title {
margin-right: 32px;
margin-inline-end: 32px;
margin-inline-start: initial;
position: relative;
padding-right: 40px;
padding-inline-end: 40px;
padding-inline-start: initial;
direction: var(--direction);
}
.header_title span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
.header_button {
position: absolute;
right: 16px;
top: 14px;
right: -8px;
top: -8px;
text-decoration: none;
color: inherit;
inset-inline-start: initial;
inset-inline-end: 16px;
inset-inline-end: -8px;
direction: var(--direction);
}
.dialog-actions {

View File

@@ -10,9 +10,9 @@ class HaLabeledSlider extends LitElement {
@property() public caption?: string;
@property() public disabled?: boolean;
@property({ type: Boolean }) public disabled = false;
@property() public required?: boolean;
@property({ type: Boolean }) public required = true;
@property() public min: number = 0;

View File

@@ -36,16 +36,24 @@ export class HaListItem extends ListItemBase {
--mdc-list-item-graphic-margin,
16px
) !important;
direction: var(--direction);
direction: var(--direction) !important;
}
span.material-icons:last-of-type {
margin-inline-start: auto !important;
margin-inline-end: 0px !important;
direction: var(--direction);
direction: var(--direction) !important;
}
.mdc-deprecated-list-item__meta {
display: var(--mdc-list-item-meta-display);
align-items: center;
flex-shrink: 0;
}
:host([graphic="icon"]:not([twoline]))
.mdc-deprecated-list-item__graphic {
margin-inline-end: var(
--mdc-list-item-graphic-margin,
20px
) !important;
}
:host([multiline-secondary]) {
height: auto;
@@ -78,6 +86,15 @@ export class HaListItem extends ListItemBase {
pointer-events: unset;
}
`,
// safari workaround - must be explicit
document.dir === "rtl"
? css`
span.material-icons:first-of-type,
span.material-icons:last-of-type {
direction: rtl !important;
}
`
: css``,
];
}
}

View File

@@ -95,6 +95,15 @@ class HaMarkdownElement extends ReactiveElement {
}
node.firstElementChild!.replaceWith(alertNote);
}
} else if (
node instanceof HTMLElement &&
["ha-alert", "ha-qr-code", "ha-icon", "ha-svg-icon"].includes(
node.localName
)
) {
import(
/* webpackInclude: /(ha-alert)|(ha-qr-code)|(ha-icon)|(ha-svg-icon)/ */ `./${node.localName}`
);
}
}
}

View File

@@ -2,11 +2,6 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-markdown-element";
// Import components that are allwoed to be defined.
import "./ha-alert";
import "./ha-icon";
import "./ha-svg-icon";
@customElement("ha-markdown")
export class HaMarkdown extends LitElement {
@property() public content?;

View File

@@ -0,0 +1,114 @@
import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import QRCode from "qrcode";
@customElement("ha-qr-code")
export class HaQrCode extends LitElement {
@property() public data?: string;
@property({ attribute: "error-correction-level" })
public errorCorrectionLevel: "low" | "medium" | "quartile" | "high" =
"medium";
@property({ type: Number })
public width = 4;
@property({ type: Number })
public scale = 4;
@property({ type: Number })
public margin = 4;
@property({ type: Number }) public maskPattern?:
| 0
| 1
| 2
| 3
| 4
| 5
| 6
| 7;
@property({ attribute: "center-image" }) public centerImage?: string;
@state() private _error?: string;
@query("canvas") private _canvas?: HTMLCanvasElement;
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (
(changedProperties.has("data") ||
changedProperties.has("scale") ||
changedProperties.has("width") ||
changedProperties.has("margin") ||
changedProperties.has("maskPattern") ||
changedProperties.has("errorCorrectionLevel")) &&
this._error
) {
this._error = undefined;
}
}
updated(changedProperties: PropertyValues) {
const canvas = this._canvas;
if (
canvas &&
this.data &&
(changedProperties.has("data") ||
changedProperties.has("scale") ||
changedProperties.has("width") ||
changedProperties.has("margin") ||
changedProperties.has("maskPattern") ||
changedProperties.has("errorCorrectionLevel") ||
changedProperties.has("centerImage"))
) {
const computedStyles = getComputedStyle(this);
QRCode.toCanvas(canvas, this.data, {
errorCorrectionLevel: this.errorCorrectionLevel,
width: this.width,
scale: this.scale,
margin: this.margin,
maskPattern: this.maskPattern,
color: {
light: computedStyles.getPropertyValue("--card-background-color"),
dark: computedStyles.getPropertyValue("--primary-text-color"),
},
}).catch((err) => {
this._error = err.message;
});
if (this.centerImage) {
const context = this._canvas!.getContext("2d");
const imageObj = new Image();
imageObj.src = this.centerImage;
imageObj.onload = () => {
context?.drawImage(
imageObj,
canvas.width * 0.375,
canvas.height * 0.375,
canvas.width / 4,
canvas.height / 4
);
};
}
}
}
render() {
if (!this.data) {
return nothing;
}
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html`<canvas></canvas>`;
}
static styles = css`
:host {
display: block;
}
`;
}

View File

@@ -43,6 +43,22 @@ export class HaNumberSelector extends LitElement {
this.selector.number?.min === undefined ||
this.selector.number?.max === undefined;
let sliderStep;
if (!isBox) {
sliderStep = this.selector.number!.step ?? 1;
if (sliderStep === "any") {
sliderStep = 1;
// divide the range of the slider by 100 steps
const step =
(this.selector.number!.max! - this.selector.number!.min!) / 100;
// biggest step size is 1, round the step size to a division of 1
while (sliderStep > step) {
sliderStep /= 10;
}
}
}
return html`
<div class="input">
${!isBox
@@ -52,12 +68,10 @@ export class HaNumberSelector extends LitElement {
: ""}
<ha-slider
labeled
.min=${this.selector.number?.min}
.max=${this.selector.number?.max}
.min=${this.selector.number!.min}
.max=${this.selector.number!.max}
.value=${this.value ?? ""}
.step=${this.selector.number?.step === "any"
? undefined
: this.selector.number?.step ?? 1}
.step=${sliderStep}
.disabled=${this.disabled}
.required=${this.required}
@change=${this._handleSliderChange}

View File

@@ -70,15 +70,15 @@ const SELECTOR_SCHEMAS = {
number: [
{
name: "min",
selector: { number: { mode: "box" } },
selector: { number: { mode: "box", step: "any" } },
},
{
name: "max",
selector: { number: { mode: "box" } },
selector: { number: { mode: "box", step: "any" } },
},
{
name: "step",
selector: { number: { mode: "box" } },
selector: { number: { mode: "box", step: "any" } },
},
] as const,
object: [] as const,

View File

@@ -4,7 +4,14 @@ import {
HassServices,
HassServiceTarget,
} from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
nothing,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../common/array/ensure-array";
@@ -83,6 +90,8 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean }) public showAdvanced?: boolean;
@property({ type: Boolean, reflect: true }) public hidePicker?: boolean;
@state() private _value!: this["value"];
@state() private _checkedKeys = new Set();
@@ -363,12 +372,14 @@ export class HaServiceControl extends LitElement {
)) ||
serviceData?.description;
return html`<ha-service-picker
.hass=${this.hass}
.value=${this._value?.service}
.disabled=${this.disabled}
@value-changed=${this._serviceChanged}
></ha-service-picker>
return html`${this.hidePicker
? nothing
: html`<ha-service-picker
.hass=${this.hass}
.value=${this._value?.service}
.disabled=${this.disabled}
@value-changed=${this._serviceChanged}
></ha-service-picker>`}
<div class="description">
${description ? html`<p>${description}</p>` : ""}
${this._manifest
@@ -522,6 +533,14 @@ export class HaServiceControl extends LitElement {
defaultValue = field.selector.constant?.value;
}
if (
defaultValue == null &&
field?.selector &&
"boolean" in field.selector
) {
defaultValue = false;
}
if (defaultValue != null) {
data = {
...this._value?.data,
@@ -727,6 +746,9 @@ export class HaServiceControl extends LitElement {
margin: var(--service-control-padding, 0 16px);
padding: 16px 0;
}
:host([hidePicker]) p {
padding-top: 0;
}
.checkbox-spacer {
width: 32px;
}

View File

@@ -35,7 +35,12 @@ export class HaSettingsRow extends LitElement {
align-items: center;
}
.body {
padding: 8px 16px 8px 0;
padding-top: 8px;
padding-bottom: 8px;
padding-left: 0;
padding-inline-start: 0;
padding-right: 16x;
padding-inline-end: 16px;
overflow: hidden;
display: var(--layout-vertical_-_display);
flex-direction: var(--layout-vertical_-_flex-direction);

View File

@@ -11,6 +11,7 @@ export class HaSlider extends MdSlider {
:host {
--md-sys-color-primary: var(--primary-color);
--md-sys-color-outline: var(--outline-color);
--md-sys-color-on-surface: var(--primary-text-color);
--md-slider-handle-width: 14px;
--md-slider-handle-height: 14px;
min-width: 100px;

View File

@@ -280,6 +280,8 @@ export abstract class TopAppBarBaseBase extends BaseElement {
}
#title {
border-right: 1px solid rgba(255, 255, 255, 0.12);
border-inline-end: 1px solid rgba(255, 255, 255, 0.12);
border-inline-start: initial;
box-sizing: border-box;
flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px);
@@ -290,6 +292,8 @@ export abstract class TopAppBarBaseBase extends BaseElement {
}
.pane {
border-right: 1px solid var(--divider-color);
border-inline-end: 1px solid var(--divider-color);
border-inline-start: initial;
box-sizing: border-box;
display: flex;
flex: 0 0 var(--sidepane-width, 250px);

View File

@@ -0,0 +1,102 @@
import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js";
import { CSSResultGroup, LitElement, html, css, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { supportsFeature } from "../common/entity/supports-feature";
import {
ValveEntity,
ValveEntityFeature,
canClose,
canOpen,
canStop,
} from "../data/valve";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
@customElement("ha-valve-controls")
class HaValveControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: ValveEntity;
protected render() {
if (!this.stateObj) {
return nothing;
}
return html`
<div class="state">
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.OPEN),
})}
.label=${this.hass.localize("ui.card.valve.open_valve")}
@click=${this._onOpenTap}
.disabled=${!canOpen(this.stateObj)}
.path=${mdiValveOpen}
>
</ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.STOP),
})}
.label=${this.hass.localize("ui.card.valve.stop_valve")}
@click=${this._onStopTap}
.disabled=${!canStop(this.stateObj)}
.path=${mdiStop}
></ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.CLOSE),
})}
.label=${this.hass.localize("ui.card.valve.close_valve")}
@click=${this._onCloseTap}
.disabled=${!canClose(this.stateObj)}
.path=${mdiValveClosed}
>
</ha-icon-button>
</div>
`;
}
private _onOpenTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "open_valve", {
entity_id: this.stateObj.entity_id,
});
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "close_valve", {
entity_id: this.stateObj.entity_id,
});
}
private _onStopTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "stop_valve", {
entity_id: this.stateObj.entity_id,
});
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
.state {
white-space: nowrap;
}
.hidden {
visibility: hidden !important;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-valve-controls": HaValveControls;
}
}

View File

@@ -62,7 +62,7 @@ class BrowseMediaTTS extends LitElement {
this.hass.localize(
"ui.components.media-browser.tts.example_message",
{
name: this.hass.user?.name || "",
name: this.hass.user?.name || "Alice",
}
)}
>

View File

@@ -53,7 +53,7 @@ class MediaUploadButton extends LitElement {
${this._uploading > 0
? html`
<ha-circular-progress
size="tiny"
size="small"
indeterminate
area-label="Uploading"
slot="icon"

View File

@@ -1,5 +1,12 @@
import { dump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
@@ -18,6 +25,12 @@ import { traceTabStyles } from "./trace-tab-styles";
import { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph";
const TRACE_PATH_TABS = [
"step_config",
"changed_variables",
"logbook",
] as const;
@customElement("ha-trace-path-details")
export class HaTracePathDetails extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -34,7 +47,7 @@ export class HaTracePathDetails extends LitElement {
@property() public trackedNodes!: Record<string, any>;
@state() private _view: "config" | "changed_variables" | "logbook" = "config";
@state() private _view: (typeof TRACE_PATH_TABS)[number] = "step_config";
protected render(): TemplateResult {
return html`
@@ -43,23 +56,21 @@ export class HaTracePathDetails extends LitElement {
</div>
<div class="tabs top">
${[
["config", "Step Config"],
["changed_variables", "Changed Variables"],
["logbook", "Related logbook entries"],
].map(
([view, label]) => html`
${TRACE_PATH_TABS.map(
(view) => html`
<button
.view=${view}
class=${classMap({ active: this._view === view })}
@click=${this._showTab}
>
${label}
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.${view}`
)}
</button>
`
)}
</div>
${this._view === "config"
${this._view === "step_config"
? this._renderSelectedConfig()
: this._view === "changed_variables"
? this._renderChangedVars()
@@ -71,7 +82,9 @@ export class HaTracePathDetails extends LitElement {
const paths = this.trace.trace;
if (!this.selected?.path) {
return "Select a node on the left for more information.";
return this.hass!.localize(
"ui.panel.config.automation.trace.path.choose"
);
}
// HACK: default choice node is not part of paths. We filter them out here by checking parent.
@@ -82,12 +95,16 @@ export class HaTracePathDetails extends LitElement {
] as ChooseActionTraceStep[];
if (parentTraceInfo && parentTraceInfo[0]?.result?.choice === "default") {
return "The default action was executed because no options matched.";
return this.hass!.localize(
"ui.panel.config.automation.trace.path.default_action_executed"
);
}
}
if (!(this.selected.path in paths)) {
return "This node was not executed and so no further trace information is available.";
return this.hass!.localize(
"ui.panel.config.automation.trace.path.no_further_execution"
);
}
const parts: TemplateResult[][] = [];
@@ -115,29 +132,53 @@ export class HaTracePathDetails extends LitElement {
trace as any;
if (result?.enabled === false) {
return html`This node was disabled and skipped during execution so
no further trace information is available.`;
return html`${this.hass!.localize(
"ui.panel.config.automation.trace.path.disabled_step"
)}`;
}
return html`
${curPath === this.selected.path
? ""
: html`<h2>${curPath.substr(this.selected.path.length + 1)}</h2>`}
${data.length === 1 ? "" : html`<h3>Iteration ${idx + 1}</h3>`}
Executed:
${formatDateTimeWithSeconds(
new Date(timestamp),
this.hass.locale,
this.hass.config
)}<br />
: html`<h2>
${curPath.substring(this.selected.path.length + 1)}
</h2>`}
${data.length === 1
? nothing
: html`<h3>
${this.hass!.localize(
"ui.panel.config.automation.trace.path.iteration",
{ number: idx + 1 }
)}
</h3>`}
${this.hass!.localize(
"ui.panel.config.automation.trace.path.executed",
{
time: formatDateTimeWithSeconds(
new Date(timestamp),
this.hass.locale,
this.hass.config
),
}
)}
<br />
${result
? html`Result:
? html`${this.hass!.localize(
"ui.panel.config.automation.trace.path.result"
)}
<pre>${dump(result)}</pre>`
: error
? html`<div class="error">Error: ${error}</div>`
: ""}
? html`<div class="error">
${this.hass!.localize(
"ui.panel.config.automation.trace.path.error",
{
error: error,
}
)}
</div>`
: nothing}
${Object.keys(rest).length === 0
? ""
? nothing
: html`<pre>${dump(rest)}</pre>`}
`;
})
@@ -149,30 +190,49 @@ export class HaTracePathDetails extends LitElement {
private _renderSelectedConfig() {
if (!this.selected?.path) {
return "";
return nothing;
}
const config = getDataFromPath(this.trace!.config, this.selected.path);
return config
? html`<ha-code-editor
.value=${dump(config).trimRight()}
.value=${dump(config).trimEnd()}
readOnly
dir="ltr"
></ha-code-editor>`
: "Unable to find config";
: this.hass!.localize(
"ui.panel.config.automation.trace.path.unable_to_find_config"
);
}
private _renderChangedVars() {
const paths = this.trace.trace;
const data: ActionTraceStep[] = paths[this.selected.path];
if (data === undefined) {
return html`<div class="padded-box">
${this.hass!.localize(
"ui.panel.config.automation.trace.path.step_not_executed"
)}
</div>`;
}
return html`
<div class="padded-box">
${data.map(
(trace, idx) => html`
${idx > 0 ? html`<p>Iteration ${idx + 1}</p>` : ""}
${data.length > 1
? html`<p>
${this.hass!.localize(
"ui.panel.config.automation.trace.path.iteration",
{ number: idx + 1 }
)}
</p>`
: ""}
${Object.keys(trace.changed_variables || {}).length === 0
? "No variables changed"
: html`<pre>${dump(trace.changed_variables).trimRight()}</pre>`}
? this.hass!.localize(
"ui.panel.config.automation.trace.path.no_variables_changed"
)
: html`<pre>${dump(trace.changed_variables).trimEnd()}</pre>`}
`
)}
</div>
@@ -186,7 +246,11 @@ export class HaTracePathDetails extends LitElement {
const index = trackedPaths.indexOf(this.selected.path);
if (index === -1) {
return html`<div class="padded-box">Node not tracked.</div>`;
return html`<div class="padded-box">
${this.hass!.localize(
"ui.panel.config.automation.trace.path.step_not_executed"
)}
</div>`;
}
let entries: LogbookEntry[];
@@ -234,7 +298,9 @@ export class HaTracePathDetails extends LitElement {
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
`
: html`<div class="padded-box">
No Logbook entries found for this step.
${this.hass!.localize(
"ui.panel.config.automation.trace.path.no_logbook_entries"
)}
</div>`;
}

View File

@@ -125,10 +125,6 @@ export class HatGraphNode extends LitElement {
:host([notEnabled]:hover) circle {
--stroke-clr: var(--disabled-hover-clr);
}
svg {
width: 100%;
height: 100%;
}
circle,
path.connector {
stroke: var(--stroke-clr);

View File

@@ -5,6 +5,8 @@ import {
mdiCallSplit,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiGestureDoubleTap,
mdiHandBackRight,
mdiPalette,
@@ -13,10 +15,12 @@ import {
mdiRoomService,
mdiShuffleDisabled,
mdiTimerOutline,
mdiTools,
mdiTrafficLight,
} from "@mdi/js";
import { AutomationElementGroup } from "./automation";
export const ACTION_TYPES = {
export const ACTION_ICONS = {
condition: mdiAbTesting,
delay: mdiTimerOutline,
event: mdiGestureDoubleTap,
@@ -34,6 +38,44 @@ export const ACTION_TYPES = {
variables: mdiApplicationVariableOutline,
} as const;
export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_TYPES>([
export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_ICONS>([
"variables",
]);
export const ACTION_GROUPS: AutomationElementGroup = {
device_id: {},
helpers: {
icon: mdiTools,
members: {},
},
building_blocks: {
icon: mdiExcavator,
members: {
condition: {},
delay: {},
wait_template: {},
wait_for_trigger: {},
repeat: {},
choose: {},
if: {},
stop: {},
parallel: {},
variables: {},
},
},
other: {
icon: mdiDotsHorizontal,
members: {
event: {},
service: {},
},
},
} as const;
export const SERVICE_PREFIX = "__SERVICE__";
export const isService = (key: string | undefined): boolean | undefined =>
key?.startsWith(SERVICE_PREFIX);
export const getService = (key: string): string =>
key.substring(SERVICE_PREFIX.length);

View File

@@ -128,7 +128,7 @@ export const areaCompare =
(entries?: HomeAssistant["areas"], order?: string[]) =>
(a: string, b: string) => {
const indexA = order ? order.indexOf(a) : -1;
const indexB = order ? order.indexOf(b) : 1;
const indexB = order ? order.indexOf(b) : -1;
if (indexA === -1 && indexB === -1) {
const nameA = entries?.[a]?.name ?? a;
const nameB = entries?.[b]?.name ?? b;

View File

@@ -275,6 +275,10 @@ export interface ShorthandNotCondition extends ShorthandBaseCondition {
not: Condition[];
}
export interface AutomationElementGroup {
[key: string]: { icon?: string; members?: AutomationElementGroup };
}
export type Condition =
| StateCondition
| NumericStateCondition

View File

@@ -766,48 +766,38 @@ const tryDescribeCondition = (
// State Condition
if (condition.condition === "state") {
let base = "Confirm";
if (!condition.entity_id) {
return `${base} state`;
return hass.localize(
`${conditionsTranslationBaseKey}.state.description.no_entity`
);
}
let attribute = "";
if (condition.attribute) {
const stateObj = Array.isArray(condition.entity_id)
? hass.states[condition.entity_id[0]]
: hass.states[condition.entity_id];
base += ` ${computeAttributeNameDisplay(
attribute = computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
condition.attribute
)} of`;
);
}
const entities: string[] = [];
if (Array.isArray(condition.entity_id)) {
const entities: string[] = [];
for (const entity of condition.entity_id.values()) {
if (hass.states[entity]) {
entities.push(computeStateName(hass.states[entity]) || entity);
}
}
if (entities.length !== 0) {
const entitiesString =
condition.match === "any"
? formatListWithOrs(hass.locale, entities)
: formatListWithAnds(hass.locale, entities);
base += ` ${entitiesString} ${
condition.entity_id.length > 1 ? "are" : "is"
}`;
} else {
// no entity_id or empty array
base += " an entity";
}
} else if (condition.entity_id) {
base += ` ${
entities.push(
hass.states[condition.entity_id]
? computeStateName(hass.states[condition.entity_id])
: condition.entity_id
} is`;
);
}
const states: string[] = [];
@@ -845,21 +835,27 @@ const tryDescribeCondition = (
);
}
if (states.length === 0) {
states.push("a state");
}
const statesString = formatListWithOrs(hass.locale, states);
base += ` ${statesString}`;
let duration = "";
if (condition.for) {
const duration = describeDuration(hass.locale, condition.for);
if (duration) {
base += ` for ${duration}`;
}
duration = describeDuration(hass.locale, condition.for) || "";
}
return base;
return hass.localize(
`${conditionsTranslationBaseKey}.state.description.full`,
{
hasAttribute: attribute !== "" ? "true" : "false",
attribute: attribute,
numberOfEntities: entities.length,
entities:
condition.match === "any"
? formatListWithOrs(hass.locale, entities)
: formatListWithAnds(hass.locale, entities),
numberOfStates: states.length,
states: formatListWithOrs(hass.locale, states),
hasDuration: duration !== "" ? "true" : "false",
duration: duration,
}
);
}
// Numeric State Condition

View File

@@ -3,16 +3,21 @@ import {
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiGateOr,
mdiIdentifier,
mdiMapClock,
mdiMapMarkerRadius,
mdiNotEqualVariant,
mdiNumeric,
mdiShape,
mdiStateMachine,
mdiWeatherSunny,
} from "@mdi/js";
import { AutomationElementGroup } from "./automation";
export const CONDITION_TYPES = {
export const CONDITION_ICONS = {
device: mdiDevices,
and: mdiAmpersand,
or: mdiGateOr,
@@ -25,3 +30,23 @@ export const CONDITION_TYPES = {
trigger: mdiIdentifier,
zone: mdiMapMarkerRadius,
};
export const CONDITION_GROUPS: AutomationElementGroup = {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { sun: {}, time: {}, zone: {} },
},
building_blocks: {
icon: mdiExcavator,
members: { and: {}, or: {}, not: {} },
},
other: {
icon: mdiDotsHorizontal,
members: {
template: {},
trigger: {},
},
},
} as const;

View File

@@ -19,14 +19,14 @@ export const enum CoverEntityFeature {
}
export function isFullyOpen(stateObj: CoverEntity) {
if (stateObj.attributes.current_position !== undefined) {
if (stateObj.attributes.current_position != null) {
return stateObj.attributes.current_position === 100;
}
return stateObj.state === "open";
}
export function isFullyClosed(stateObj: CoverEntity) {
if (stateObj.attributes.current_position !== undefined) {
if (stateObj.attributes.current_position != null) {
return stateObj.attributes.current_position === 0;
}
return stateObj.state === "closed";
@@ -65,7 +65,7 @@ export function canOpen(stateObj: CoverEntity) {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return (!isFullyOpen(stateObj) && !isOpening(stateObj)) || assumedState;
return assumedState || (!isFullyOpen(stateObj) && !isOpening(stateObj));
}
export function canClose(stateObj: CoverEntity): boolean {
@@ -73,7 +73,7 @@ export function canClose(stateObj: CoverEntity): boolean {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return (!isFullyClosed(stateObj) && !isClosing(stateObj)) || assumedState;
return assumedState || (!isFullyClosed(stateObj) && !isClosing(stateObj));
}
export function canStop(stateObj: CoverEntity): boolean {
@@ -85,7 +85,7 @@ export function canOpenTilt(stateObj: CoverEntity): boolean {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return !isFullyOpenTilt(stateObj) || assumedState;
return assumedState || !isFullyOpenTilt(stateObj);
}
export function canCloseTilt(stateObj: CoverEntity): boolean {
@@ -93,7 +93,7 @@ export function canCloseTilt(stateObj: CoverEntity): boolean {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return !isFullyClosedTilt(stateObj) || assumedState;
return assumedState || !isFullyClosedTilt(stateObj);
}
export function canStopTilt(stateObj: CoverEntity): boolean {

View File

@@ -75,6 +75,9 @@ export const DOMAIN_ATTRIBUTES_UNITS = {
vacuum: {
battery_level: "%",
},
valve: {
current_position: "%",
},
sensor: {
battery_level: "%",
},

View File

@@ -16,7 +16,9 @@ export type IntegrationType =
| "helper"
| "hub"
| "service"
| "hardware";
| "hardware"
| "entity"
| "system";
export interface IntegrationManifest {
is_built_in: boolean;

View File

@@ -90,7 +90,7 @@ export const enum MediaPlayerEntityFeature {
TURN_ON = 128,
TURN_OFF = 256,
PLAY_MEDIA = 512,
VOLUME_BUTTONS = 1024,
VOLUME_STEP = 1024,
SELECT_SOURCE = 2048,
STOP = 4096,
CLEAR_PLAYLIST = 8192,

View File

@@ -52,6 +52,7 @@ export const serviceActionStruct: Describe<ServiceAction> = assign(
target: optional(targetStruct),
data: optional(object()),
response_variable: optional(string()),
metadata: optional(object()),
})
);
@@ -133,6 +134,7 @@ export interface ServiceAction extends BaseAction {
target?: HassServiceTarget;
data?: Record<string, unknown>;
response_variable?: string;
metadata?: Record<string, unknown>;
}
export interface DeviceAction extends BaseAction {

View File

@@ -168,6 +168,18 @@ const tryDescribeAction = <T extends ActionType>(
const service =
hass.localize(`component.${domain}.services.${serviceName}.name`) ||
hass.services[domain][serviceName]?.name;
if (config.metadata) {
return hass.localize(
`${actionTranslationBaseKey}.service.description.service_name`,
{
domain: domainToName(hass.localize, domain),
name: service || config.service,
targets: formatListWithAnds(hass.locale, targets),
}
);
}
return hass.localize(
`${actionTranslationBaseKey}.service.description.service_based_on_name`,
{
@@ -404,7 +416,9 @@ const tryDescribeAction = <T extends ActionType>(
if (actionType === "device_action") {
const config = action as DeviceAction;
if (!config.device_id) {
return "Device action";
return hass.localize(
`${actionTranslationBaseKey}.device_id.description.no_device`
);
}
const localized = localizeDeviceAutomationAction(
hass,

View File

@@ -18,6 +18,8 @@ export interface TodoItem {
uid: string;
summary: string;
status: TodoItemStatus;
description?: string | null;
due?: string | null;
}
export const enum TodoListEntityFeature {
@@ -25,6 +27,9 @@ export const enum TodoListEntityFeature {
DELETE_TODO_ITEM = 2,
UPDATE_TODO_ITEM = 4,
MOVE_TODO_ITEM = 8,
SET_DUE_DATE_ON_ITEM = 16,
SET_DUE_DATETIME_ON_ITEM = 32,
SET_DESCRIPTION_ON_ITEM = 64,
}
export const getTodoLists = (hass: HomeAssistant): TodoList[] =>
@@ -74,20 +79,36 @@ export const updateItem = (
hass.callService(
"todo",
"update_item",
{ item: item.uid, rename: item.summary, status: item.status },
{
item: item.uid,
rename: item.summary,
status: item.status,
description: item.description,
due_datetime: item.due?.includes("T") ? item.due : undefined,
due_date:
item.due === undefined || item.due?.includes("T")
? undefined
: item.due,
},
{ entity_id }
);
export const createItem = (
hass: HomeAssistant,
entity_id: string,
summary: string
item: Omit<TodoItem, "uid" | "status">
): Promise<ServiceCallResponse> =>
hass.callService(
"todo",
"add_item",
{
item: summary,
item: item.summary,
description: item.description || undefined,
due_datetime: item.due?.includes("T") ? item.due : undefined,
due_date:
item.due === undefined || item.due?.includes("T")
? undefined
: item.due,
},
{ entity_id }
);

View File

@@ -4,13 +4,16 @@ import {
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiGestureDoubleTap,
mdiMapClock,
mdiMapMarker,
mdiMapMarkerRadius,
mdiMessageAlert,
mdiMicrophoneMessage,
mdiNfcVariant,
mdiNumeric,
mdiShape,
mdiStateMachine,
mdiSwapHorizontal,
mdiWeatherSunny,
@@ -18,8 +21,9 @@ import {
} from "@mdi/js";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import { AutomationElementGroup } from "./automation";
export const TRIGGER_TYPES = {
export const TRIGGER_ICONS = {
calendar: mdiCalendar,
device: mdiDevices,
event: mdiGestureDoubleTap,
@@ -38,3 +42,26 @@ export const TRIGGER_TYPES = {
persistent_notification: mdiMessageAlert,
zone: mdiMapMarkerRadius,
};
export const TRIGGER_GROUPS: AutomationElementGroup = {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { calendar: {}, sun: {}, time: {}, time_pattern: {}, zone: {} },
},
other: {
icon: mdiDotsHorizontal,
members: {
event: {},
geo_location: {},
homeassistant: {},
mqtt: {},
conversation: {},
tag: {},
template: {},
webhook: {},
persistent_notification: {},
},
},
} as const;

View File

@@ -13,11 +13,13 @@ import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
export const UPDATE_SUPPORT_INSTALL = 1;
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
export const UPDATE_SUPPORT_PROGRESS = 4;
export const UPDATE_SUPPORT_BACKUP = 8;
export const UPDATE_SUPPORT_RELEASE_NOTES = 16;
export enum UpdateEntityFeature {
INSTALL = 1,
SPECIFIC_VERSION = 2,
PROGRESS = 4,
BACKUP = 8,
RELEASE_NOTES = 16,
}
interface UpdateEntityAttributes extends HassEntityAttributeBase {
auto_update: boolean | null;
@@ -35,7 +37,7 @@ export interface UpdateEntity extends HassEntityBase {
}
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
typeof entity.attributes.in_progress === "number";
export const updateCanInstall = (
@@ -44,7 +46,7 @@ export const updateCanInstall = (
): boolean =>
(entity.state === BINARY_STATE_ON ||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
supportsFeature(entity, UPDATE_SUPPORT_INSTALL);
supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
updateUsesProgress(entity) || !!entity.attributes.in_progress;
@@ -176,7 +178,7 @@ export const computeUpdateStateDisplay = (
if (state === "on") {
if (updateIsInstalling(stateObj)) {
const supportsProgress =
supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS) &&
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
typeof attributes.in_progress === "number";
if (supportsProgress) {
return hass.localize("ui.card.update.installing_with_progress", {

85
src/data/valve.ts Normal file
View File

@@ -0,0 +1,85 @@
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { UNAVAILABLE } from "./entity";
import { stateActive } from "../common/entity/state_active";
import { HomeAssistant } from "../types";
export const enum ValveEntityFeature {
OPEN = 1,
CLOSE = 2,
SET_POSITION = 4,
STOP = 8,
}
export function isFullyOpen(stateObj: ValveEntity) {
if (stateObj.attributes.current_position != null) {
return stateObj.attributes.current_position === 100;
}
return stateObj.state === "open";
}
export function isFullyClosed(stateObj: ValveEntity) {
if (stateObj.attributes.current_position != null) {
return stateObj.attributes.current_position === 0;
}
return stateObj.state === "closed";
}
export function isOpening(stateObj: ValveEntity) {
return stateObj.state === "opening";
}
export function isClosing(stateObj: ValveEntity) {
return stateObj.state === "closing";
}
export function canOpen(stateObj: ValveEntity) {
if (stateObj.state === UNAVAILABLE) {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return assumedState || (!isFullyOpen(stateObj) && !isOpening(stateObj));
}
export function canClose(stateObj: ValveEntity): boolean {
if (stateObj.state === UNAVAILABLE) {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return assumedState || (!isFullyClosed(stateObj) && !isClosing(stateObj));
}
export function canStop(stateObj: ValveEntity): boolean {
return stateObj.state !== UNAVAILABLE;
}
interface ValveEntityAttributes extends HassEntityAttributeBase {
current_position?: number;
position?: number;
}
export interface ValveEntity extends HassEntityBase {
attributes: ValveEntityAttributes;
}
export function computeValvePositionStateDisplay(
stateObj: ValveEntity,
hass: HomeAssistant,
position?: number
) {
const statePosition = stateActive(stateObj)
? stateObj.attributes.current_position
: undefined;
const currentPosition = position ?? statePosition;
return currentPosition && currentPosition !== 100
? hass.formatEntityAttributeValue(
stateObj,
"current_position",
Math.round(currentPosition)
)
: "";
}

View File

@@ -383,11 +383,13 @@ export const removeMembersFromGroup = (
export const addGroup = (
hass: HomeAssistant,
groupName: string,
groupId?: number,
membersToAdd?: ZHAGroupMember[]
): Promise<ZHAGroup> =>
hass.callWS({
type: "zha/group/add",
group_name: groupName,
group_id: groupId,
members: membersToAdd,
});

View File

@@ -207,7 +207,8 @@ export class DialogAreaFilter
color: var(--disabled-text-color);
}
.handle {
cursor: move;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
.actions {
display: flex;

View File

@@ -27,6 +27,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"lock",
"siren",
"switch",
"valve",
"water_heater",
];
/** Domains with separate more info dialog. */
@@ -61,6 +62,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"timer",
"update",
"vacuum",
"valve",
"water_heater",
"weather",
];

View File

@@ -42,7 +42,9 @@ class MoreInfoCover extends LitElement {
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("stateObj") && this.stateObj) {
if (!this._mode) {
const entityId = this.stateObj.entity_id;
const oldEntityId = changedProps.get("stateObj")?.entity_id;
if (!this._mode || entityId !== oldEntityId) {
this._mode =
supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION) ||
supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION)

View File

@@ -81,7 +81,7 @@ class MoreInfoMediaPlayer extends LitElement {
: ""}
</div>
${(supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET) ||
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_BUTTONS)) &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP)) &&
stateActive(stateObj)
? html`
<div class="volume">
@@ -104,8 +104,9 @@ class MoreInfoMediaPlayer extends LitElement {
: ""}
${supportsFeature(
stateObj,
MediaPlayerEntityFeature.VOLUME_BUTTONS
)
MediaPlayerEntityFeature.VOLUME_SET
) ||
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP)
? html`
<ha-icon-button
action="volume_down"

View File

@@ -13,13 +13,9 @@ import "../../../components/ha-markdown";
import { isUnavailableState } from "../../../data/entity";
import {
UpdateEntity,
UpdateEntityFeature,
updateIsInstalling,
updateReleaseNotes,
UPDATE_SUPPORT_BACKUP,
UPDATE_SUPPORT_INSTALL,
UPDATE_SUPPORT_PROGRESS,
UPDATE_SUPPORT_RELEASE_NOTES,
UPDATE_SUPPORT_SPECIFIC_VERSION,
} from "../../../data/update";
import type { HomeAssistant } from "../../../types";
@@ -49,7 +45,7 @@ class MoreInfoUpdate extends LitElement {
return html`
${this.stateObj.attributes.in_progress
? supportsFeature(this.stateObj, UPDATE_SUPPORT_PROGRESS) &&
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
typeof this.stateObj.attributes.in_progress === "number"
? html`<mwc-linear-progress
.progress=${this.stateObj.attributes.in_progress / 100}
@@ -101,7 +97,7 @@ class MoreInfoUpdate extends LitElement {
</div>
</div>`
: ""}
${supportsFeature(this.stateObj!, UPDATE_SUPPORT_RELEASE_NOTES) &&
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error
? !this._releaseNotes
? html`<div class="flex center">
@@ -117,7 +113,7 @@ class MoreInfoUpdate extends LitElement {
.content=${this.stateObj.attributes.release_summary}
></ha-markdown>`
: ""}
${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP)
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
? html`<hr />
<ha-formfield
.label=${this.hass.localize(
@@ -155,7 +151,7 @@ class MoreInfoUpdate extends LitElement {
)}
</mwc-button>
`}
${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL)
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
? html`
<mwc-button
@click=${this._handleInstall}
@@ -174,7 +170,7 @@ class MoreInfoUpdate extends LitElement {
}
protected firstUpdated(): void {
if (supportsFeature(this.stateObj!, UPDATE_SUPPORT_RELEASE_NOTES)) {
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
updateReleaseNotes(this.hass, this.stateObj!.entity_id)
.then((result) => {
this._releaseNotes = result;
@@ -186,7 +182,7 @@ class MoreInfoUpdate extends LitElement {
}
get _shouldCreateBackup(): boolean | null {
if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) {
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return null;
}
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
@@ -206,7 +202,7 @@ class MoreInfoUpdate extends LitElement {
}
if (
supportsFeature(this.stateObj!, UPDATE_SUPPORT_SPECIFIC_VERSION) &&
supportsFeature(this.stateObj!, UpdateEntityFeature.SPECIFIC_VERSION) &&
this.stateObj!.attributes.latest_version
) {
installData.version = this.stateObj!.attributes.latest_version;

View File

@@ -0,0 +1,192 @@
import { mdiMenu, mdiSwapVertical } from "@mdi/js";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attributes";
import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle";
import {
ValveEntity,
ValveEntityFeature,
computeValvePositionStateDisplay,
} from "../../../data/valve";
import "../../../state-control/valve/ha-state-control-valve-buttons";
import "../../../state-control/valve/ha-state-control-valve-position";
import "../../../state-control/valve/ha-state-control-valve-toggle";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-state-header";
import { moreInfoControlStyle } from "../components/more-info-control-style";
type Mode = "position" | "button";
@customElement("more-info-valve")
class MoreInfoValve extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: ValveEntity;
@state() private _mode?: Mode;
private _setMode(ev) {
this._mode = ev.currentTarget.mode;
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("stateObj") && this.stateObj) {
const entityId = this.stateObj.entity_id;
const oldEntityId = changedProps.get("stateObj")?.entity_id;
if (!this._mode || entityId !== oldEntityId) {
this._mode = supportsFeature(
this.stateObj,
ValveEntityFeature.SET_POSITION
)
? "position"
: "button";
}
}
}
private get _stateOverride() {
const stateDisplay = this.hass.formatEntityState(this.stateObj!);
const positionStateDisplay = computeValvePositionStateDisplay(
this.stateObj!,
this.hass
);
if (positionStateDisplay) {
return `${stateDisplay}${positionStateDisplay}`;
}
return stateDisplay;
}
protected render() {
if (!this.hass || !this.stateObj) {
return nothing;
}
const supportsPosition = supportsFeature(
this.stateObj,
ValveEntityFeature.SET_POSITION
);
const supportsOpenClose =
supportsFeature(this.stateObj, ValveEntityFeature.OPEN) ||
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) ||
supportsFeature(this.stateObj, ValveEntityFeature.STOP);
const supportsOpenCloseWithoutStop =
supportsFeature(this.stateObj, ValveEntityFeature.OPEN) &&
supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) &&
!supportsFeature(this.stateObj, ValveEntityFeature.STOP);
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateOverride=${this._stateOverride}
></ha-more-info-state-header>
<div class="controls">
<div class="main-control">
${
this._mode === "position"
? html`
${supportsPosition
? html`
<ha-state-control-valve-position
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-position>
`
: nothing}
`
: nothing
}
${
this._mode === "button"
? html`
${supportsOpenCloseWithoutStop
? html`
<ha-state-control-valve-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-toggle>
`
: supportsOpenClose
? html`
<ha-state-control-valve-buttons
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-buttons>
`
: nothing}
`
: nothing
}
</div>
${
supportsPosition && supportsOpenClose
? html`
<ha-icon-button-group>
<ha-icon-button-toggle
.label=${this.hass.localize(
`ui.dialogs.more_info_control.valve.switch_mode.position`
)}
.selected=${this._mode === "position"}
.path=${mdiMenu}
.mode=${"position"}
@click=${this._setMode}
></ha-icon-button-toggle>
<ha-icon-button-toggle
.label=${this.hass.localize(
`ui.dialogs.more_info_control.valve.switch_mode.button`
)}
.selected=${this._mode === "button"}
.path=${mdiSwapVertical}
.mode=${"button"}
@click=${this._setMode}
></ha-icon-button-toggle>
</ha-icon-button-group>
`
: nothing
}
</div>
</div>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="current_position,current_tilt_position"
></ha-attributes>
`;
}
static get styles(): CSSResultGroup {
return [
moreInfoControlStyle,
css`
.main-control {
display: flex;
flex-direction: row;
align-items: center;
}
.main-control > * {
margin: 0 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-valve": MoreInfoValve;
}
}

View File

@@ -35,6 +35,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
timer: () => import("./controls/more-info-timer"),
update: () => import("./controls/more-info-update"),
vacuum: () => import("./controls/more-info-vacuum"),
valve: () => import("./controls/more-info-valve"),
water_heater: () => import("./controls/more-info-water_heater"),
weather: () => import("./controls/more-info-weather"),
};

View File

@@ -0,0 +1,92 @@
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-button";
import { createCloseHeading } from "../../components/ha-dialog";
import { HomeAssistant } from "../../types";
import { UpdateBackupDialogParams } from "./show-update-backup-dialog";
@customElement("dialog-update-backup")
class DialogBox extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: UpdateBackupDialogParams;
public async showDialog(params: UpdateBackupDialogParams): Promise<void> {
this._params = params;
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this._cancel}
defaultAction="ignore"
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.update_backup.title")
)}
>
<p>${this.hass.localize("ui.dialogs.update_backup.text")}</p>
<ha-button @click=${this._no} slot="secondaryAction">
${this.hass!.localize("ui.common.no")}
</ha-button>
<ha-button @click=${this._yes} slot="primaryAction">
${this.hass.localize("ui.dialogs.update_backup.create")}
</ha-button>
</ha-dialog>
`;
}
private _no(): void {
if (this._params!.submit) {
this._params!.submit(false);
}
this.closeDialog();
}
private _yes(): void {
if (this._params!.submit) {
this._params!.submit(true);
}
this.closeDialog();
}
private _cancel(): void {
this._params?.cancel?.();
this.closeDialog();
}
public closeDialog(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
static get styles(): CSSResultGroup {
return css`
p {
margin: 0;
color: var(--primary-text-color);
}
ha-dialog {
/* Place above other dialogs */
--dialog-z-index: 104;
}
@media all and (min-width: 600px) {
ha-dialog {
--mdc-dialog-min-width: 400px;
}
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-update-backup": DialogBox;
}
}

View File

@@ -0,0 +1,35 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface UpdateBackupDialogParams {
submit?: (response: boolean) => void;
cancel?: () => void;
}
export const showUpdateBackupDialogParams = (
element: HTMLElement,
dialogParams: UpdateBackupDialogParams
) =>
new Promise<boolean | null>((resolve) => {
const origCancel = dialogParams.cancel;
const origSubmit = dialogParams.submit;
fireEvent(element, "show-dialog", {
dialogTag: "dialog-update-backup",
dialogImport: () => import("./dialog-update-backup"),
dialogParams: {
...dialogParams,
cancel: () => {
resolve(null);
if (origCancel) {
origCancel();
}
},
submit: (response: boolean) => {
resolve(response);
if (origSubmit) {
origSubmit(response);
}
},
},
});
});

View File

@@ -668,7 +668,12 @@ export class HaVoiceCommandDialog extends LitElement {
ha-button-menu {
--mdc-theme-on-primary: var(--text-primary-color);
--mdc-theme-primary: var(--primary-color);
margin: -8px 0 0 -8px;
margin-top: -8px;
margin-bottom: 0;
margin-right: 0;
margin-inline-end: 0;
margin-left: -8px;
margin-inline-start: -8px;
}
ha-button-menu ha-button {
--mdc-theme-primary: var(--secondary-text-color);
@@ -689,7 +694,7 @@ export class HaVoiceCommandDialog extends LitElement {
height: 28px;
margin-left: 4px;
margin-inline-start: 4px;
margin-inline-end: 4px;
margin-inline-end: initial;
direction: var(--direction);
}
ha-list-item {
@@ -698,7 +703,7 @@ export class HaVoiceCommandDialog extends LitElement {
ha-list-item ha-svg-icon {
margin-left: 4px;
margin-inline-start: 4px;
margin-inline-end: 4px;
margin-inline-end: initial;
direction: var(--direction);
display: block;
}

View File

@@ -124,6 +124,12 @@ export class HaTabsSubpageDataTable extends LitElement {
*/
@property({ type: String }) public noDataText?: string;
/**
* Hides the data table and show an empty message.
* @type {Boolean}
*/
@property({ type: Boolean }) public empty = false;
@property() public route!: Route;
/**
@@ -198,56 +204,61 @@ export class HaTabsSubpageDataTable extends LitElement {
.mainPage=${this.mainPage}
.supervisor=${this.supervisor}
>
${!this.hideFilterMenu
? html`
<div slot="toolbar-icon">
${this.narrow
${this.empty
? html`<div class="center">
<slot name="empty">${this.noDataText}</slot>
</div>`
: html`${!this.hideFilterMenu
? html`
<div slot="toolbar-icon">
${this.narrow
? html`
<div class="filter-menu">
${this.numHidden || this.activeFilters
? html`<span class="badge"
>${this.numHidden || "!"}</span
>`
: ""}
<slot name="filter-menu"></slot>
</div>
`
: ""}<slot name="toolbar-icon"></slot>
</div>
`
: ""}
${this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="search-toolbar">${headerToolbar}</div>
</slot>
</div>
`
: ""}
<ha-data-table
.hass=${this.hass}
.columns=${this.columns}
.data=${this.data}
.noDataText=${this.noDataText}
.filter=${this.filter}
.selectable=${this.selectable}
.hasFab=${this.hasFab}
.id=${this.id}
.dir=${computeRTLDirection(this.hass)}
.clickable=${this.clickable}
.appendRow=${this.appendRow}
>
${!this.narrow
? html`
<div class="filter-menu">
${this.numHidden || this.activeFilters
? html`<span class="badge"
>${this.numHidden || "!"}</span
>`
: ""}
<slot name="filter-menu"></slot>
<div slot="header">
<slot name="header">
<div class="table-header">${headerToolbar}</div>
</slot>
</div>
`
: ""}<slot name="toolbar-icon"></slot>
</div>
`
: ""}
${this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="search-toolbar">${headerToolbar}</div>
</slot>
</div>
`
: ""}
<ha-data-table
.hass=${this.hass}
.columns=${this.columns}
.data=${this.data}
.filter=${this.filter}
.selectable=${this.selectable}
.hasFab=${this.hasFab}
.id=${this.id}
.noDataText=${this.noDataText}
.dir=${computeRTLDirection(this.hass)}
.clickable=${this.clickable}
.appendRow=${this.appendRow}
>
${!this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="table-header">${headerToolbar}</div>
</slot>
</div>
`
: html` <div slot="header"></div> `}
</ha-data-table>
: html` <div slot="header"></div> `}
</ha-data-table>`}
<div slot="fab"><slot name="fab"></slot></div>
</hass-tabs-subpage>
`;
@@ -374,6 +385,16 @@ export class HaTabsSubpageDataTable extends LitElement {
.filter-menu {
position: relative;
}
.center {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
box-sizing: border-box;
height: 100%;
width: 100%;
padding: 16px;
}
`;
}
}

View File

@@ -1,8 +1,8 @@
import "@material/mwc-button";
import { mdiCalendarClock, mdiClose } from "@mdi/js";
import { mdiCalendarClock } from "@mdi/js";
import { toDate } from "date-fns-tz";
import { addDays, isSameDay } from "date-fns/esm";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators";
import { formatDate } from "../../common/datetime/format_date";
import { formatDateTime } from "../../common/datetime/format_date_time";
@@ -11,6 +11,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { isDate } from "../../common/string/is_date";
import "../../components/entity/state-info";
import "../../components/ha-date-input";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-time-input";
import {
CalendarEventMutableParams,
@@ -65,15 +66,7 @@ class DialogCalendarEventDetail extends LitElement {
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${html`
<div class="header_title">${this._data!.summary}</div>
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
dialogAction="close"
class="header_button"
></ha-icon-button>
`}
.heading=${createCloseHeading(this.hass, this._data!.summary)}
>
<div class="content">
${this._error

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button";
import { mdiClose } from "@mdi/js";
import { formatInTimeZone, toDate } from "date-fns-tz";
import {
addDays,
@@ -9,7 +8,7 @@ import {
startOfHour,
} from "date-fns/esm";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
@@ -18,23 +17,24 @@ import { supportsFeature } from "../../common/entity/supports-feature";
import { isDate } from "../../common/string/is_date";
import "../../components/entity/ha-entity-picker";
import "../../components/ha-date-input";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-textarea";
import "../../components/ha-time-input";
import {
CalendarEntityFeature,
CalendarEventMutableParams,
RecurrenceRange,
createCalendarEvent,
deleteCalendarEvent,
RecurrenceRange,
updateCalendarEvent,
} from "../../data/calendar";
import { TimeZone } from "../../data/translation";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import "../lovelace/components/hui-generic-entity-row";
import "./ha-recurrence-rule-editor";
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
import { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor";
import { TimeZone } from "../../data/translation";
const CALENDAR_DOMAINS = ["calendar"];
@@ -142,19 +142,12 @@ class DialogCalendarEventEditor extends LitElement {
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${html`
<div class="header_title">
${isCreate
? this.hass.localize("ui.components.calendar.event.add")
: this._summary}
</div>
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
dialogAction="close"
class="header_button"
></ha-icon-button>
`}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
`ui.components.calendar.event.${isCreate ? "add" : "edit"}`
)
)}
>
<div class="content">
${this._error
@@ -584,9 +577,11 @@ class DialogCalendarEventEditor extends LitElement {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-min-width: min(600px, 95vw);
--mdc-dialog-max-width: min(600px, 95vw);
@media all and (min-width: 450px and min-height: 500px) {
ha-dialog {
--mdc-dialog-min-width: min(600px, 95vw);
--mdc-dialog-max-width: min(600px, 95vw);
}
}
state-info {
line-height: 40px;

View File

@@ -29,6 +29,8 @@ 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 { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIconWithoutDefault } from "../../../../common/entity/domain_icon";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-alert";
@@ -37,7 +39,7 @@ import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_TYPES, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
@@ -82,9 +84,9 @@ export const getType = (action: Action | undefined) => {
if (["and", "or", "not"].some((key) => key in action)) {
return "condition" as const;
}
return Object.keys(ACTION_TYPES).find(
return Object.keys(ACTION_ICONS).find(
(option) => option in action
) as keyof typeof ACTION_TYPES;
) as keyof typeof ACTION_ICONS;
};
export interface ActionElement extends LitElement {
@@ -190,7 +192,13 @@ export default class HaAutomationActionRow extends LitElement {
<h3 slot="header">
<ha-svg-icon
class="action-icon"
.path=${ACTION_TYPES[type!]}
.path=${type === "service" &&
"service" in this.action &&
this.action.service
? domainIconWithoutDefault(
computeDomain(this.action.service as string)
) || ACTION_ICONS[type!]
: ACTION_ICONS[type!]}
></ha-svg-icon>
${capitalizeFirstLetter(
describeAction(this.hass, this._entityReg, this.action)

View File

@@ -1,57 +1,26 @@
import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentPaste,
mdiDrag,
mdiPlus,
} from "@mdi/js";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import { ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation";
import { getService, isService } from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation";
import { Action } from "../../../../data/script";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
import { Entries, HomeAssistant } from "../../../../types";
import { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
import type HaAutomationActionRow from "./ha-automation-action-row";
import { getType } from "./ha-automation-action-row";
import "./types/ha-automation-action-activate_scene";
import "./types/ha-automation-action-choose";
import "./types/ha-automation-action-condition";
import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-event";
import "./types/ha-automation-action-if";
import "./types/ha-automation-action-parallel";
import "./types/ha-automation-action-play_media";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-service";
import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
const PASTE_VALUE = "__paste__";
@customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement {
@@ -150,42 +119,27 @@ export default class HaAutomationAction extends LitElement {
`
)}
</div>
<ha-button-menu
@action=${this._addAction}
.disabled=${this.disabled}
fixed
>
<div class="buttons">
<ha-button
slot="trigger"
outlined
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.add"
)}
@click=${this._addActionDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${this._clipboard?.action
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.paste"
)}
(${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${
getType(this._clipboard.action) || "unknown"
}.label`
)})
<ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon
></mwc-list-item>`
: nothing}
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`
)}
</ha-button-menu>
<ha-button
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.add_building_block"
)}
@click=${this._addActionBuildingBlockDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
</div>
`;
}
@@ -213,6 +167,44 @@ export default class HaAutomationAction extends LitElement {
}
}
private _addActionDialog() {
showAddAutomationElementDialog(this, {
type: "action",
add: this._addAction,
clipboardItem: getType(this._clipboard?.action),
});
}
private _addActionBuildingBlockDialog() {
showAddAutomationElementDialog(this, {
type: "action",
add: this._addAction,
clipboardItem: getType(this._clipboard?.action),
group: "building_blocks",
});
}
private _addAction = (action: string) => {
let actions: Action[];
if (action === PASTE_VALUE) {
actions = this.actions.concat(deepClone(this._clipboard!.action));
} else if (isService(action)) {
actions = this.actions.concat({
service: getService(action),
metadata: {},
});
} else {
const elClass = customElements.get(
`ha-automation-action-${action}`
) as CustomElementConstructor & { defaultConfig: Action };
actions = this.actions.concat(
elClass ? { ...elClass.defaultConfig } : { [action]: {} }
);
}
this._focusLastActionOnChange = true;
fireEvent(this, "value-changed", { value: actions });
};
private async _enterReOrderMode(ev: CustomEvent) {
if (this.nested) return;
ev.stopPropagation();
@@ -258,25 +250,6 @@ export default class HaAutomationAction extends LitElement {
return this._actionKeys.get(action)!;
}
private _addAction(ev: CustomEvent<ActionDetail>) {
const action = (ev.currentTarget as HaSelect).items[ev.detail.index].value;
let actions: Action[];
if (action === PASTE_VALUE) {
actions = this.actions.concat(deepClone(this._clipboard!.action));
} else {
const elClass = customElements.get(
`ha-automation-action-${action}`
) as CustomElementConstructor & { defaultConfig: Action };
actions = this.actions.concat(
elClass ? { ...elClass.defaultConfig } : { [action]: {} }
);
}
this._focusLastActionOnChange = true;
fireEvent(this, "value-changed", { value: actions });
}
private _moveUp(ev) {
const index = (ev.target as any).index;
const newIndex = index - 1;
@@ -328,22 +301,6 @@ export default class HaAutomationAction extends LitElement {
});
}
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(ACTION_TYPES) as Entries<typeof ACTION_TYPES>)
.map(
([action, icon]) =>
[
action,
localize(
`ui.panel.config.automation.editor.actions.type.${action}.label`
),
icon,
] as [string, string, string]
)
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
);
static get styles(): CSSResultGroup {
return [
sortableStyles,
@@ -363,13 +320,19 @@ export default class HaAutomationAction extends LitElement {
overflow: hidden;
}
.handle {
cursor: move;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
`,
];
}

View File

@@ -543,7 +543,8 @@ export class HaChooseAction extends LitElement implements ActionElement {
padding: 0 16px 16px 16px;
}
.handle {
cursor: move;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {

View File

@@ -7,7 +7,7 @@ import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select";
import type { Condition } from "../../../../../data/automation";
import { CONDITION_TYPES } from "../../../../../data/condition";
import { CONDITION_ICONS } from "../../../../../data/condition";
import { Entries, HomeAssistant } from "../../../../../types";
import "../../condition/ha-automation-condition-editor";
import type { ActionElement } from "../ha-automation-action-row";
@@ -55,7 +55,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>)
(Object.entries(CONDITION_ICONS) as Entries<typeof CONDITION_ICONS>)
.map(
([condition, icon]) =>
[

View File

@@ -9,6 +9,7 @@ import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row";
import { isTemplate } from "../../../../../common/string/has-template";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
@@ -32,7 +33,12 @@ export class HaRepeatAction extends LitElement implements ActionElement {
}
private _schema = memoizeOne(
(localize: LocalizeFunc, type: string, reOrderMode: boolean) =>
(
localize: LocalizeFunc,
type: string,
reOrderMode: boolean,
template: boolean
) =>
[
{
name: "type",
@@ -53,7 +59,9 @@ export class HaRepeatAction extends LitElement implements ActionElement {
{
name: "count",
required: true,
selector: { number: { mode: "box", min: 1 } },
selector: template
? ({ template: {} } as const)
: ({ number: { mode: "box", min: 1 } } as const),
},
] as const)
: []),
@@ -89,10 +97,13 @@ export class HaRepeatAction extends LitElement implements ActionElement {
const schema = this._schema(
this.hass.localize,
type ?? "count",
this.reOrderMode
this.reOrderMode,
"count" in action && typeof action.count === "string"
? isTemplate(action.count)
: false
);
const data = { ...action, type };
return html` <ha-form
return html`<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}

View File

@@ -0,0 +1,674 @@
import "@material/mwc-list/mwc-list";
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
import Fuse, { IFuseOptions } from "fuse.js";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { domainIconWithoutDefault } from "../../../common/entity/domain_icon";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import { stringCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-dialog";
import type { HaDialog } from "../../../components/ha-dialog";
import "../../../components/ha-dialog-header";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
import "../../../components/ha-list-item";
import "../../../components/search-input";
import {
ACTION_GROUPS,
ACTION_ICONS,
SERVICE_PREFIX,
getService,
isService,
} from "../../../data/action";
import { AutomationElementGroup } from "../../../data/automation";
import { CONDITION_GROUPS, CONDITION_ICONS } from "../../../data/condition";
import {
IntegrationManifest,
domainToName,
fetchIntegrationManifests,
} from "../../../data/integration";
import { TRIGGER_GROUPS, TRIGGER_ICONS } from "../../../data/trigger";
import { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import {
AddAutomationElementDialogParams,
PASTE_VALUE,
} from "./show-add-automation-element-dialog";
import { computeDomain } from "../../../common/entity/compute_domain";
import { deepEqual } from "../../../common/util/deep-equal";
const TYPES = {
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
condition: {
groups: CONDITION_GROUPS,
icons: CONDITION_ICONS,
},
action: {
groups: ACTION_GROUPS,
icons: ACTION_ICONS,
},
};
interface ListItem {
key: string;
name: string;
description: string;
icon?: string;
image?: string;
group: boolean;
}
interface DomainManifestLookup {
[domain: string]: IntegrationManifest;
}
const ENTITY_DOMAINS_OTHER = new Set([
"date",
"datetime",
"device_tracker",
"text",
"time",
"tts",
"update",
"weather",
"image_processing",
]);
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
@customElement("add-automation-element-dialog")
class DialogAddAutomationElement extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: AddAutomationElementDialogParams;
@state() private _group?: string;
@state() private _prev?: string;
@state() private _filter = "";
@state() private _manifests?: DomainManifestLookup;
@state() private _domains?: Set<string>;
@query("ha-dialog") private _dialog?: HaDialog;
private _fullScreen = false;
@state() private _width?: number;
@state() private _height?: number;
public showDialog(params): void {
this._params = params;
this._group = params.group;
if (this._params?.type === "action") {
this.hass.loadBackendTranslation("services");
this._fetchManifests();
this._calculateUsedDomains();
}
this._fullScreen = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
}
public closeDialog(): void {
if (this._params) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._height = undefined;
this._width = undefined;
this._params = undefined;
this._group = undefined;
this._prev = undefined;
this._filter = "";
this._manifests = undefined;
this._domains = undefined;
}
private _getGroups = (
type: AddAutomationElementDialogParams["type"],
group: string | undefined
): AutomationElementGroup =>
group
? isService(group)
? {}
: TYPES[type].groups[group].members!
: TYPES[type].groups;
private _convertToItem = (
key: string,
options,
type: AddAutomationElementDialogParams["type"],
localize: LocalizeFunc
): ListItem => ({
group: Boolean(options.members),
key,
name: localize(
// @ts-ignore
`ui.panel.config.automation.editor.${type}s.${
options.members ? "groups" : "type"
}.${key}.label`
),
description: localize(
// @ts-ignore
`ui.panel.config.automation.editor.${type}s.${
options.members ? "groups" : "type"
}.${key}.description${options.members ? "" : ".picker"}`
),
icon: options.icon || TYPES[type].icons[key],
});
private _getFilteredItems = memoizeOne(
(
type: AddAutomationElementDialogParams["type"],
group: string | undefined,
filter: string,
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests?: DomainManifestLookup
): ListItem[] => {
const groups = this._getGroups(type, group);
const flattenGroups = (grp: AutomationElementGroup) =>
Object.entries(grp).map(([key, options]) =>
options.members
? flattenGroups(options.members)
: this._convertToItem(key, options, type, localize)
);
const items = flattenGroups(groups).flat();
if (type === "action") {
items.push(...this._services(localize, services, manifests, group));
}
const options: IFuseOptions<ListItem> = {
keys: ["key", "name", "description"],
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
};
const fuse = new Fuse(items, options);
return fuse.search(filter).map((result) => result.item);
}
);
private _getGroupItems = memoizeOne(
(
type: AddAutomationElementDialogParams["type"],
group: string | undefined,
domains: Set<string> | undefined,
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests?: DomainManifestLookup
): ListItem[] => {
if (type === "action" && isService(group)) {
let result = this._services(localize, services, manifests, group);
if (group === `${SERVICE_PREFIX}media_player`) {
result = [
this._convertToItem("play_media", {}, type, localize),
...result,
];
}
return result;
}
const groups = this._getGroups(type, group);
const result = Object.entries(groups).map(([key, options]) =>
this._convertToItem(key, options, type, localize)
);
if (type === "action") {
if (!this._group) {
result.unshift(
...this._serviceGroups(
localize,
services,
manifests,
domains,
undefined
)
);
} else if (this._group === "helpers") {
result.unshift(
...this._serviceGroups(
localize,
services,
manifests,
domains,
"helper"
)
);
} else if (this._group === "other") {
result.unshift(
...this._serviceGroups(
localize,
services,
manifests,
domains,
"other"
)
);
}
}
return result.sort((a, b) => {
if (a.group && b.group) {
return 0;
}
if (a.group && !b.group) {
return 1;
}
if (!a.group && b.group) {
return -1;
}
return stringCompare(a.name, b.name, this.hass.locale.language);
});
}
);
private _serviceGroups = (
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests: DomainManifestLookup | undefined,
domains: Set<string> | undefined,
type: "helper" | "other" | undefined
): ListItem[] => {
if (!services || !manifests) {
return [];
}
const result: ListItem[] = [];
Object.keys(services).forEach((domain) => {
const manifest = manifests[domain];
const domainUsed = !domains ? true : domains.has(domain);
if (
(type === undefined &&
(ENTITY_DOMAINS_MAIN.has(domain) ||
(manifest?.integration_type === "entity" &&
domainUsed &&
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
(type === "helper" && manifest?.integration_type === "helper") ||
(type === "other" &&
!ENTITY_DOMAINS_MAIN.has(domain) &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
(!domainUsed && manifest?.integration_type === "entity") ||
!["helper", "entity"].includes(manifest?.integration_type || "")))
) {
const icon = domainIconWithoutDefault(domain);
result.push({
group: true,
icon,
image: !icon
? brandsUrl({
domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})
: undefined,
key: `${SERVICE_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
});
}
});
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
};
private _services = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests: DomainManifestLookup | undefined,
group?: string
): ListItem[] => {
if (!services) {
return [];
}
const result: ListItem[] = [];
let domain: string | undefined;
if (isService(group)) {
domain = getService(group!);
}
const addDomain = (dmn: string) => {
const services_keys = Object.keys(services[dmn]);
for (const service of services_keys) {
const icon = domainIconWithoutDefault(dmn);
result.push({
group: false,
icon,
image: !icon
? brandsUrl({
domain: dmn,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})
: undefined,
key: `${SERVICE_PREFIX}${dmn}.${service}`,
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
services[dmn][service]?.name ||
service
}`,
description:
this.hass.localize(
`component.${domain}.services.${service}.description`
) || services[dmn][service]?.description,
});
}
};
if (domain) {
addDomain(domain);
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
}
if (group && !["helpers", "other"].includes(group)) {
return [];
}
Object.keys(services)
.sort()
.forEach((dmn) => {
const manifest = manifests?.[dmn];
if (group === "helpers" && manifest?.integration_type !== "helper") {
return;
}
if (
group === "other" &&
(ENTITY_DOMAINS_OTHER.has(dmn) ||
["helper", "entity"].includes(manifest?.integration_type || ""))
) {
return;
}
addDomain(dmn);
});
return result;
}
);
private async _fetchManifests() {
const manifests = {};
const fetched = await fetchIntegrationManifests(this.hass);
for (const manifest of fetched) {
manifests[manifest.domain] = manifest;
}
this._manifests = manifests;
}
private _calculateUsedDomains() {
const domains = new Set(Object.keys(this.hass.states).map(computeDomain));
if (!deepEqual(domains, this._domains)) {
this._domains = domains;
}
}
protected _opened(): void {
// Store the width and height so that when we search, box doesn't jump
const boundingRect =
this.shadowRoot!.querySelector("mwc-list")?.getBoundingClientRect();
this._width = boundingRect?.width;
this._height = boundingRect?.height;
}
protected willUpdate(changedProperties: PropertyValues): void {
if (
this._params?.type === "action" &&
changedProperties.has("hass") &&
changedProperties.get("hass")?.states !== this.hass.states
) {
this._calculateUsedDomains();
}
}
protected render() {
if (!this._params) {
return nothing;
}
const items = this._filter
? this._getFilteredItems(
this._params.type,
this._group,
this._filter,
this.hass.localize,
this.hass.services,
this._manifests
)
: this._getGroupItems(
this._params.type,
this._group,
this._domains,
this.hass.localize,
this.hass.services,
this._manifests
);
const groupName = isService(this._group)
? domainToName(
this.hass.localize,
getService(this._group!),
this._manifests?.[getService(this._group!)]
)
: this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${this._params.type}s.groups.${this._group}.label`
);
return html`
<ha-dialog
open
hideActions
@opened=${this._opened}
@closed=${this.closeDialog}
.heading=${true}
>
<div slot="heading">
<ha-dialog-header>
<span slot="title"
>${this._group
? groupName
: this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.add`
)}</span
>
${this._group && this._group !== this._params.group
? html`<ha-icon-button-prev
slot="navigationIcon"
@click=${this._back}
></ha-icon-button-prev>`
: html`<ha-icon-button
.path=${mdiClose}
slot="navigationIcon"
dialogAction="cancel"
></ha-icon-button>`}
</ha-dialog-header>
<search-input
dialogInitialFocus=${ifDefined(this._fullScreen ? undefined : "")}
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._filterChanged}
.label=${groupName
? this.hass.localize(
"ui.panel.config.automation.editor.search_in",
{ group: groupName }
)
: this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.search`
)}
></search-input>
</div>
<mwc-list
dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)}
innerRole="listbox"
itemRoles="option"
rootTabbable
style=${styleMap({
width: this._width ? `${this._width}px` : "auto",
height: this._height ? `${Math.min(468, this._height)}px` : "auto",
})}
>
${this._params.clipboardItem &&
!this._filter &&
(!this._group ||
items.find((item) => item.key === this._params!.clipboardItem))
? html`<ha-list-item
twoline
class="paste"
.value=${PASTE_VALUE}
graphic="icon"
hasMeta
@request-selected=${this._selected}
>
${this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.paste`
)}
<span slot="secondary"
>${this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
)}</span
>
<ha-svg-icon
slot="graphic"
.path=${mdiContentPaste}
></ha-svg-icon
><ha-svg-icon slot="meta" .path=${mdiPlus}></ha-svg-icon>
</ha-list-item>
<li divider role="separator"></li>`
: ""}
${repeat(
items,
(item) => item.key,
(item) => html`
<ha-list-item
.twoline=${Boolean(item.description)}
.value=${item.key}
.group=${item.group}
graphic="icon"
hasMeta
@request-selected=${this._selected}
>
${item.name}
<span slot="secondary">${item.description}</span>
${item.icon
? html`<ha-svg-icon
slot="graphic"
.path=${item.icon}
></ha-svg-icon>`
: html`<img
alt=""
slot="graphic"
src=${item.image}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>`}
${item.group
? html`<ha-icon-next slot="meta"></ha-icon-next>`
: html`<ha-svg-icon
slot="meta"
.path=${mdiPlus}
></ha-svg-icon>`}
</ha-list-item>
`
)}
</mwc-list>
</ha-dialog>
`;
}
private _back() {
this._dialog!.scrollToPos(0, 0);
if (this._filter) {
this._filter = "";
return;
}
if (this._prev) {
this._group = this._prev;
this._prev = undefined;
return;
}
this._group = undefined;
}
private _selected(ev) {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._dialog!.scrollToPos(0, 0);
const item = ev.currentTarget;
if (item.group) {
this._prev = this._group;
this._group = item.value;
return;
}
this._params!.add(item.value);
this.closeDialog();
}
private _filterChanged(ev) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
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;
}
mwc-list {
max-height: 468px;
max-width: 100vw;
}
search-input {
display: block;
margin: 0 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"add-automation-element-dialog": DialogAddAutomationElement;
}
}

View File

@@ -29,7 +29,7 @@ import "../../../../components/ha-icon-button";
import type { AutomationClipboard } from "../../../../data/automation";
import { Condition, testCondition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n";
import { CONDITION_TYPES } from "../../../../data/condition";
import { CONDITION_ICONS } from "../../../../data/condition";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
@@ -123,7 +123,7 @@ export default class HaAutomationConditionRow extends LitElement {
<h3 slot="header">
<ha-svg-icon
class="condition-icon"
.path=${CONDITION_TYPES[this.condition.condition]}
.path=${CONDITION_ICONS[this.condition.condition]}
></ha-svg-icon>
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)

View File

@@ -1,25 +1,18 @@
import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentPaste,
mdiDrag,
mdiPlus,
} from "@mdi/js";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
@@ -28,30 +21,15 @@ import type {
AutomationClipboard,
Condition,
} from "../../../../data/automation";
import type { Entries, HomeAssistant } from "../../../../types";
import "./ha-automation-condition-row";
import type HaAutomationConditionRow from "./ha-automation-condition-row";
// Uncommenting these and this element doesn't load
// import "./types/ha-automation-condition-not";
// import "./types/ha-automation-condition-or";
import { storage } from "../../../../common/decorators/storage";
import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaSelect } from "../../../../components/ha-select";
import { CONDITION_TYPES } from "../../../../data/condition";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
import "./types/ha-automation-condition-and";
import "./types/ha-automation-condition-device";
import "./types/ha-automation-condition-numeric_state";
import "./types/ha-automation-condition-state";
import "./types/ha-automation-condition-sun";
import "./types/ha-automation-condition-template";
import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
const PASTE_VALUE = "__paste__";
import type { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
import "./ha-automation-condition-row";
import type HaAutomationConditionRow from "./ha-automation-condition-row";
@customElement("ha-automation-condition")
export default class HaAutomationCondition extends LitElement {
@@ -197,43 +175,69 @@ export default class HaAutomationCondition extends LitElement {
`
)}
</div>
<ha-button-menu
@action=${this._addCondition}
.disabled=${this.disabled}
fixed
>
<div class="buttons">
<ha-button
slot="trigger"
outlined
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.add"
)}
@click=${this._addConditionDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${this._clipboard?.condition
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.paste"
)}
(${this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.${this._clipboard.condition.condition}.label`
)})
<ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon
></mwc-list-item>`
: nothing}
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`
)}
</ha-button-menu>
<ha-button
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.add_building_block"
)}
@click=${this._addConditionBuildingBlockDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
</div>
`;
}
private _addConditionDialog() {
showAddAutomationElementDialog(this, {
type: "condition",
add: this._addCondition,
clipboardItem: this._clipboard?.condition?.condition,
});
}
private _addConditionBuildingBlockDialog() {
showAddAutomationElementDialog(this, {
type: "condition",
add: this._addCondition,
clipboardItem: this._clipboard?.condition?.condition,
group: "building_blocks",
});
}
private _addCondition = (value) => {
let conditions: Condition[];
if (value === PASTE_VALUE) {
conditions = this.conditions.concat(
deepClone(this._clipboard!.condition)
);
} else {
const condition = value as Condition["condition"];
const elClass = customElements.get(
`ha-automation-condition-${condition}`
) as CustomElementConstructor & {
defaultConfig: Omit<Condition, "condition">;
};
conditions = this.conditions.concat({
condition: condition as any,
...elClass.defaultConfig,
});
}
this._focusLastConditionOnChange = true;
fireEvent(this, "value-changed", { value: conditions });
};
private async _enterReOrderMode(ev: CustomEvent) {
if (this.nested) return;
ev.stopPropagation();
@@ -282,32 +286,6 @@ export default class HaAutomationCondition extends LitElement {
return this._conditionKeys.get(condition)!;
}
private _addCondition(ev: CustomEvent<ActionDetail>) {
const value = (ev.currentTarget as HaSelect).items[ev.detail.index].value;
let conditions: Condition[];
if (value === PASTE_VALUE) {
conditions = this.conditions.concat(
deepClone(this._clipboard!.condition)
);
} else {
const condition = value as Condition["condition"];
const elClass = customElements.get(
`ha-automation-condition-${condition}`
) as CustomElementConstructor & {
defaultConfig: Omit<Condition, "condition">;
};
conditions = this.conditions.concat({
condition: condition as any,
...elClass.defaultConfig,
});
}
this._focusLastConditionOnChange = true;
fireEvent(this, "value-changed", { value: conditions });
}
private _moveUp(ev) {
const index = (ev.target as any).index;
const newIndex = index - 1;
@@ -361,22 +339,6 @@ export default class HaAutomationCondition extends LitElement {
});
}
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>)
.map(
([condition, icon]) =>
[
condition,
localize(
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
),
icon,
] as [string, string, string]
)
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
);
static get styles(): CSSResultGroup {
return [
sortableStyles,
@@ -396,13 +358,19 @@ export default class HaAutomationCondition extends LitElement {
overflow: hidden;
}
.handle {
cursor: move;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
`,
];
}

View File

@@ -168,6 +168,7 @@ export class HaDeviceCondition extends LitElement {
}
ha-form {
display: block;
margin-top: 24px;
}
`;

View File

@@ -486,7 +486,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
value.valid
? ""
: html`${this.hass.localize(
`ui.panel.config.automation.editor.${key}s.header`
`ui.panel.config.automation.editor.${key}s.name`
)}:
${value.error}<br />`
);

View File

@@ -7,11 +7,19 @@ import {
mdiPlay,
mdiPlayCircleOutline,
mdiPlus,
mdiRobotHappy,
mdiStopCircleOutline,
mdiTransitConnection,
} from "@mdi/js";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { differenceInDays } from "date-fns/esm";
@@ -295,6 +303,7 @@ class HaAutomationPicker extends LitElement {
.activeFilters=${this._activeFilters}
.columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._automations(this.automations, this._filteredAutomations)}
.empty=${!this.automations.length}
@row-click=${this._handleRowClicked}
.noDataText=${this.hass.localize(
"ui.panel.config.automation.picker.no_automations"
@@ -318,6 +327,36 @@ class HaAutomationPicker extends LitElement {
@related-changed=${this._relatedFilterChanged}
>
</ha-button-related-filter-menu>
${!this.automations.length
? html`<div class="empty" slot="empty">
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
<h1>
${this.hass.localize(
"ui.panel.config.automation.picker.empty_header"
)}
</h1>
<p>
${this.hass.localize(
"ui.panel.config.automation.picker.empty_text_1"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.automation.picker.empty_text_2",
{ user: this.hass.user?.name || "Alice" }
)}
</p>
<a
href=${documentationUrl(this.hass, "/docs/automation/editor/")}
target="_blank"
rel="noreferrer"
>
<ha-button>
${this.hass.localize("ui.panel.config.common.learn_more")}
</ha-button>
</a>
</div>`
: nothing}
<ha-fab
slot="fab"
.label=${this.hass.localize(
@@ -505,7 +544,16 @@ class HaAutomationPicker extends LitElement {
}
static get styles(): CSSResultGroup {
return haStyle;
return [
haStyle,
css`
.empty {
--paper-font-headline_-_font-size: 28px;
--mdc-icon-size: 80px;
max-width: 500px;
}
`,
];
}
}

View File

@@ -7,7 +7,14 @@ import {
mdiRayStartArrow,
mdiRefresh,
} from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
@@ -41,6 +48,8 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { computeRTL } from "../../../common/util/compute_rtl";
const TABS = ["details", "automation_config", "timeline", "logbook"] as const;
@customElement("ha-automation-trace")
export class HaAutomationTrace extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -67,12 +76,7 @@ export class HaAutomationTrace extends LitElement {
@state() private _logbookEntries?: LogbookEntry[];
@state() private _view:
| "details"
| "config"
| "timeline"
| "logbook"
| "blueprint" = "details";
@state() private _view: (typeof TABS)[number] | "blueprint" = "details";
@query("hat-script-graph") private _graph?: HatScriptGraph;
@@ -213,9 +217,15 @@ export class HaAutomationTrace extends LitElement {
</div>
${this._traces === undefined
? html`<div class="container">Loading…</div>`
? html`<div class="container">
${this.hass!.localize("ui.common.loading")}
</div>`
: this._traces.length === 0
? html`<div class="container">No traces found</div>`
? html`<div class="container">
${this.hass!.localize(
"ui.panel.config.automation.trace.no_traces_found"
)}
</div>`
: this._trace === undefined
? ""
: html`
@@ -230,20 +240,17 @@ export class HaAutomationTrace extends LitElement {
<div class="info">
<div class="tabs top">
${[
["details", "Step Details"],
["timeline", "Trace Timeline"],
["logbook", "Related logbook entries"],
["config", "Automation Config"],
].map(
([view, label]) => html`
${TABS.map(
(view) => html`
<button
tabindex="0"
.view=${view}
class=${classMap({ active: this._view === view })}
@click=${this._showTab}
>
${label}
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.${view}`
)}
</button>
`
)}
@@ -257,7 +264,9 @@ export class HaAutomationTrace extends LitElement {
})}
@click=${this._showTab}
>
Blueprint Config
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.blueprint_config`
)}
</button>
`
: ""}
@@ -265,7 +274,7 @@ export class HaAutomationTrace extends LitElement {
${this._selected === undefined ||
this._logbookEntries === undefined ||
trackedNodes === undefined
? ""
? nothing
: this._view === "details"
? html`
<ha-trace-path-details
@@ -278,7 +287,7 @@ export class HaAutomationTrace extends LitElement {
.renderedNodes=${renderedNodes!}
></ha-trace-path-details>
`
: this._view === "config"
: this._view === "automation_config"
? html`
<ha-trace-config
.hass=${this.hass}

View File

@@ -1,11 +1,13 @@
import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../../common/array/ensure-array";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-markdown";
import {
Condition,
ManualAutomationConfig,
@@ -83,6 +85,13 @@ export class HaManualAutomationEditor extends LitElement {
></ha-icon-button>
</a>
</div>
${!ensureArray(this.config.trigger)?.length
? html`<p>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.description"
)}
</p>`
: nothing}
<ha-automation-trigger
role="region"
@@ -98,6 +107,9 @@ export class HaManualAutomationEditor extends LitElement {
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header"
)}
<span class="small"
>(${this.hass.localize("ui.common.optional")})</span
>
</h2>
<a
href=${documentationUrl(this.hass, "/docs/automation/condition/")}
@@ -112,6 +124,14 @@ export class HaManualAutomationEditor extends LitElement {
></ha-icon-button>
</a>
</div>
${!ensureArray(this.config.condition)?.length
? html`<p>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.description",
{ user: this.hass.user?.name || "Alice" }
)}
</p>`
: nothing}
<ha-automation-condition
role="region"
@@ -143,6 +163,13 @@ export class HaManualAutomationEditor extends LitElement {
</a>
</div>
</div>
${!ensureArray(this.config.action)?.length
? html`<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.description"
)}
</p>`
: nothing}
<ha-automation-action
role="region"
@@ -207,9 +234,11 @@ export class HaManualAutomationEditor extends LitElement {
margin: 0;
}
p {
margin-bottom: 0;
margin-top: 0;
}
.header {
margin-top: 16px;
display: flex;
align-items: center;
}
@@ -217,13 +246,18 @@ export class HaManualAutomationEditor extends LitElement {
margin-top: -16px;
}
.header .name {
font-size: 20px;
font-weight: 400;
flex: 1;
margin-bottom: 16px;
}
.header a {
color: var(--secondary-text-color);
}
.header .small {
font-size: small;
font-weight: normal;
line-height: 0;
}
`,
];
}

View File

@@ -0,0 +1,22 @@
import { fireEvent } from "../../../common/dom/fire_event";
export const PASTE_VALUE = "__paste__";
export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action";
add: (key: string) => void;
clipboardItem: string | undefined;
group?: string;
}
const loadDialog = () => import("./add-automation-element-dialog");
export const showAddAutomationElementDialog = (
element: HTMLElement,
dialogParams: AddAutomationElementDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "add-automation-element-dialog",
dialogImport: loadDialog,
dialogParams,
});
};

View File

@@ -37,7 +37,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_TYPES } from "../../../../data/trigger";
import { TRIGGER_ICONS } from "../../../../data/trigger";
import {
showAlertDialog,
showConfirmationDialog,
@@ -150,7 +150,7 @@ export default class HaAutomationTriggerRow extends LitElement {
<h3 slot="header">
<ha-svg-icon
class="trigger-icon"
.path=${TRIGGER_TYPES[this.trigger.platform]}
.path=${TRIGGER_ICONS[this.trigger.platform]}
></ha-svg-icon>
${describeTrigger(this.trigger, this.hass, this._entityReg)}
</h3>

View File

@@ -1,59 +1,25 @@
import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentPaste,
mdiDrag,
mdiPlus,
} from "@mdi/js";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import { AutomationClipboard, Trigger } from "../../../../data/automation";
import { TRIGGER_TYPES } from "../../../../data/trigger";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
import { Entries, HomeAssistant } from "../../../../types";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import "./types/ha-automation-trigger-calendar";
import "./types/ha-automation-trigger-conversation";
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-mqtt";
import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-persistent_notification";
import "./types/ha-automation-trigger-state";
import "./types/ha-automation-trigger-sun";
import "./types/ha-automation-trigger-tag";
import "./types/ha-automation-trigger-template";
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";
const PASTE_VALUE = "__paste__";
import {
PASTE_VALUE,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
@customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends LitElement {
@@ -147,47 +113,48 @@ export default class HaAutomationTrigger extends LitElement {
</ha-automation-trigger-row>
`
)}
<ha-button-menu
@action=${this._addTrigger}
.disabled=${this.disabled}
fixed
>
<ha-button
slot="trigger"
outlined
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.add"
)}
.disabled=${this.disabled}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${this._clipboard?.trigger
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.paste"
)}
(${this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.${this._clipboard.trigger.platform}.label`
)})
<ha-svg-icon
slot="graphic"
.path=${mdiContentPaste}
></ha-svg-icon
></mwc-list-item>`
: nothing}
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`
<ha-button
outlined
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.add"
)}
</ha-button-menu>
.disabled=${this.disabled}
@click=${this._addTriggerDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
</div>
`;
}
private _addTriggerDialog() {
showAddAutomationElementDialog(this, {
type: "trigger",
add: this._addTrigger,
clipboardItem: this._clipboard?.trigger?.platform,
});
}
private _addTrigger = (value: string) => {
let triggers: Trigger[];
if (value === PASTE_VALUE) {
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
} else {
const platform = value as Trigger["platform"];
const elClass = customElements.get(
`ha-automation-trigger-${platform}`
) as CustomElementConstructor & {
defaultConfig: Omit<Trigger, "platform">;
};
triggers = this.triggers.concat({
platform: platform as any,
...elClass.defaultConfig,
});
}
this._focusLastTriggerOnChange = true;
fireEvent(this, "value-changed", { value: triggers });
};
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
@@ -261,30 +228,6 @@ export default class HaAutomationTrigger extends LitElement {
return this._triggerKeys.get(action)!;
}
private _addTrigger(ev: CustomEvent<ActionDetail>) {
const value = (ev.currentTarget as HaSelect).items[ev.detail.index].value;
let triggers: Trigger[];
if (value === PASTE_VALUE) {
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
} else {
const platform = value as Trigger["platform"];
const elClass = customElements.get(
`ha-automation-trigger-${platform}`
) as CustomElementConstructor & {
defaultConfig: Omit<Trigger, "platform">;
};
triggers = this.triggers.concat({
platform: platform as any,
...elClass.defaultConfig,
});
}
this._focusLastTriggerOnChange = true;
fireEvent(this, "value-changed", { value: triggers });
}
private _moveUp(ev) {
const index = (ev.target as any).index;
const newIndex = index - 1;
@@ -336,22 +279,6 @@ export default class HaAutomationTrigger extends LitElement {
});
}
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(TRIGGER_TYPES) as Entries<typeof TRIGGER_TYPES>)
.map(
([action, icon]) =>
[
action,
localize(
`ui.panel.config.automation.editor.triggers.type.${action}.label`
),
icon,
] as [string, string, string]
)
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
);
static get styles(): CSSResultGroup {
return [
sortableStyles,
@@ -371,7 +298,8 @@ export default class HaAutomationTrigger extends LitElement {
overflow: hidden;
}
.handle {
cursor: move;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {

View File

@@ -174,6 +174,7 @@ export class HaDeviceTrigger extends LitElement {
}
ha-form {
display: block;
margin-top: 24px;
}
`;

View File

@@ -109,9 +109,11 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
${this.narrow && entity.attributes.in_progress
? html`<ha-circular-progress
indeterminate
size="small"
slot="graphic"
class="absolute"
.ariaLabel=${this.hass.localize(
"ui.panel.config.updates.update_in_progress"
)}
></ha-circular-progress>`
: ""}
<span
@@ -131,6 +133,9 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
indeterminate
size="small"
slot="meta"
.ariaLabel=${this.hass.localize(
"ui.panel.config.updates.update_in_progress"
)}
></ha-circular-progress>`
: html`<ha-icon-next slot="meta"></ha-icon-next>`
: ""}
@@ -191,6 +196,8 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
}
ha-circular-progress.absolute {
position: absolute;
width: 40px;
height: 40px;
}
state-badge.updating {
opacity: 0.5;

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