Compare commits

..

175 Commits

Author SHA1 Message Date
Zack Barett
876fd9e85a Bumped version to 20220425.0 (#12425) 2022-04-25 18:25:07 -05:00
Zack Barett
e8c30cabca Show usage stats in System Health (#12424) 2022-04-25 23:24:58 +00:00
Bram Kragten
490f84a7b1 link to updates page (#12423) 2022-04-25 15:56:34 -05:00
Artem Sorokin
ca28178b86 Fix title and description for menu step in options flow (#12420) 2022-04-25 20:26:05 +00:00
Zack Barett
2fceb0aeee Allow for checking for updates (#12422) 2022-04-25 22:15:26 +02:00
Bram Kragten
86f39d1d43 Add supervisor, OS version info to about page (#12421)
* Add supervisor, OS version info to about page

* description

* description
2022-04-25 22:14:32 +02:00
Zack Barett
1faf60444d Move Data Disk Moving to Storage (#12416) 2022-04-25 20:49:44 +02:00
Netzwerkfehler
e927091d21 Better gauge segment coloring (#11570) 2022-04-25 13:43:53 -05:00
Bram Kragten
cff2f856b3 Don't show tabs in supervisor (#12417) 2022-04-25 13:37:48 -05:00
Bram Kragten
a743e3bbba Show what updates are skipped (#12418) 2022-04-25 18:37:24 +00:00
Zack Barett
f8a52d250e Move System Health to a page (#12412) 2022-04-25 20:26:53 +02:00
Zack Barett
b70a523bdf Backup Page - Will load which is available (#12414) 2022-04-25 19:54:11 +02:00
Zack Barett
8f2ed747e6 Configuration Menu Cleanup items (#12413) 2022-04-25 19:53:02 +02:00
Zack Barett
5deccefb15 Allow Showing Skipped Updates on Updates Page (#12415) 2022-04-25 19:50:30 +02:00
Zack Barett
3f04abfa9d Add Supervisor logs to core page (#12410) 2022-04-25 15:35:03 +00:00
Zack Barett
8e55c83996 Add Hardware Page to Configuration System Menu (#12405) 2022-04-25 17:30:53 +02:00
Bram Kragten
dee59486ba Add supervisor hostname config (#12407) 2022-04-25 10:27:38 -05:00
Bram Kragten
77ef509aea Fix zones (#12409) 2022-04-25 15:13:31 +00:00
Bram Kragten
bfa7bccfa6 Add supervisor network interface settings (#12403) 2022-04-25 16:21:03 +02:00
Thomas Lovén
a8c365edc8 Fix broken cards being able to crash entire view (#11440) 2022-04-25 14:37:32 +02:00
Bram Kragten
94953ddf6c Hide supervisor only config, fix backup config page (#12401) 2022-04-25 07:09:23 -05:00
Zack Barett
6b67546daf Virtualize Media Player Grid (#11898) 2022-04-25 12:32:50 +02:00
Paulus Schoutsen
3e188d1f87 Add shorthand condition to the gallery (#12400) 2022-04-25 10:00:28 +02:00
Zack Barett
f69eb15a90 Config Menu: Addressing Comments in #12377 (#12399) 2022-04-24 20:25:47 -07:00
Paulus Schoutsen
dfe348187f Bumped version to 20220424.0 2022-04-24 15:26:42 -07:00
Zack Barett
9706c56c5c Configuration Menu Updates 3 (#12377) 2022-04-24 15:26:01 -07:00
Raman Gupta
3677c5be2c Update zwavejs controller model (#12390) 2022-04-24 14:55:31 -07:00
Zack Barett
bd339fa963 Fix Dashboard URLs (#12394) 2022-04-24 14:55:04 -07:00
Yosi Levy
28f1b6bdf4 Force LTR on time & number inputs (#12393) 2022-04-23 19:48:17 -05:00
Zack Barett
c5aac3b81d Add Empty list item for None (#12356) 2022-04-23 10:11:44 -05:00
yangqian
70836597e9 Show vacuum state in more-info dialog for StateVacuumEntity (#12391) 2022-04-23 06:32:59 +00:00
Allen Porter
958a1de2fd Add calendar event end trigger to automation editor (#12389) 2022-04-23 00:19:23 -05:00
Allen Porter
36d30266e3 Add automation editor for calendar trigger (#12343) 2022-04-22 16:53:45 -05:00
Yosi Levy
558ab9761d RTL reading orders and alignments in system log (#12388) 2022-04-22 20:19:38 +00:00
Eric Stern
269ef370e4 Accept new value when hitting ENTER to close a prompt dialog (#12360)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-04-22 14:23:34 -05:00
Kuba Wolanin
ba2958ecd2 zwave_js: Add title tag to config box heading (#12387) 2022-04-22 13:17:42 -05:00
Yosi Levy
3b8b6eb315 RTL fixes (#12367) 2022-04-22 10:27:49 -05:00
Mark Lopez
4f13db3178 Added ability to retry on initialization errors. (#12103) 2022-04-21 21:11:32 -05:00
Joakim Sørensen
ee7aa54ab4 Add entity search tip to dev-tools set state (#12355) 2022-04-21 21:06:35 -05:00
Wesley Vos
c305dd4cd5 Fix for monetary entities (#12378) 2022-04-21 21:01:09 -05:00
Franck Nijhof
6865791596 Add jinja2 editor to template triggers/conditions (#12365)
Co-authored-by: Zack <zackbarett@hey.com>
2022-04-22 01:42:28 +00:00
Franck Nijhof
2099259393 Use template selector in wait_template (#12366) 2022-04-21 20:32:25 -05:00
Zack Barett
27ca45dc70 Bumped version to 20220420.0 (#12369) 2022-04-20 22:43:40 +00:00
Zack Barett
d290c11219 Config menu updates to get it ready for nightly (#12368) 2022-04-20 22:38:35 +00:00
Zack Barett
cabe10ffdb Getting started on Configuration Changes (#12309)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-04-20 16:57:51 -05:00
Franck Nijhof
aa562c21a8 Add stop script/automation action (#12299) 2022-04-20 16:50:09 -05:00
Franck Nijhof
22175a7271 Add if/else automation/script action (#12301)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-04-20 11:27:16 -05:00
Eric Stern
1e0647c0d1 Github no longer supports the (insecure) git protocol (#12359) 2022-04-20 13:52:12 +00:00
Simon Vallières
58d94da8b3 Adding blueprint input description markdown/multi-line support (#12291) 2022-04-20 08:36:18 -05:00
Joakim Sørensen
d97763a3e8 Add clear skipped to update more-info dialog (#12361) 2022-04-20 08:17:56 -05:00
Paulus Schoutsen
aa129aa123 Bump HAWS to 7.0.3 (#12358) 2022-04-19 12:50:18 -05:00
Franck Nijhof
f648317206 Fix strict error handling in developer tools templates (#12352) 2022-04-19 07:28:07 -05:00
Raman Gupta
0685fdf7c6 Add basic frontend support for siren (#12345) 2022-04-18 11:19:01 -05:00
Franck Nijhof
6fd4cda534 Add Template selector (#12348) 2022-04-18 11:17:44 -05:00
Franck Nijhof
511368da13 Allow selecting multiple entities for state trigger (#12334)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-04-15 19:03:14 +00:00
J. Nick Koston
76e1721c58 Quickly search for entities from the Overview Dashboard (#12324) 2022-04-15 13:54:57 -05:00
Yosi Levy
bad5a389b5 RTL calendar fix - arrows fix and views fix (#12314)
* RTL calendar fix - arrows fix and views fix

* Removed path attributes
2022-04-15 13:47:46 -05:00
Paulus Schoutsen
85d1f49763 Allow tapping on the name on a picture entity card (#12332) 2022-04-15 08:55:22 -05:00
Paulus Schoutsen
7723d47ac1 Split only on first comma in media browser (#12331) 2022-04-14 17:52:49 -05:00
J. Nick Koston
30b130ca74 Use new mdi icons for smoke and co detection (#12323) 2022-04-14 15:42:54 -07:00
Joakim Sørensen
a124ec0717 Always render title field (#12319) 2022-04-13 07:20:00 -05:00
Joakim Sørensen
323d98ecf7 Decode view path URL (#12310) 2022-04-12 06:52:17 -05:00
Paulus Schoutsen
125a601ae3 Select default mode if none set (#12306) 2022-04-11 12:08:37 -05:00
Paulus Schoutsen
3c549c6b31 Update cloud text (#12305) 2022-04-11 09:47:31 -07:00
Kuba Wolanin
9c1494c74d Fix endless loading screen in zwave-js config (#12295) 2022-04-11 16:56:03 +02:00
Philip Allgaier
e751abd775 Prevent empty brackets if no manufacturer during config entry creation (#12288) 2022-04-11 09:06:29 -05:00
Joakim Sørensen
714f2447b7 Use more text selector types for add-on configuration (#12303) 2022-04-11 16:01:30 +02:00
Franck Nijhof
d900e40d04 Fix add-on security rating range (#12300) 2022-04-11 14:04:54 +02:00
Joakim Sørensen
8b82383790 Guard for partial translations (#12296) 2022-04-11 13:03:41 +02:00
Zack Barett
5a2cc2646c Merge pull request #12252 from spacegaier/issue-12246 2022-04-07 16:40:08 -05:00
Philip Allgaier
16a0902989 Adjust import 2022-04-07 22:41:37 +02:00
Philip Allgaier
8f67aa38af Fix entity and device selector with multiple: true 2022-04-07 21:39:04 +02:00
Zack Barett
34184cf2ab Merge pull request #12250 from spacegaier/issue-12248 2022-04-07 14:11:29 -05:00
Philip Allgaier
611cd2818e Exclude hidden entities from area card 2022-04-07 20:58:21 +02:00
Zack Barett
0a4e8fd5d0 Merge pull request #12244 from home-assistant/lineup-badges 2022-04-07 13:19:48 -05:00
Ludeeus
11f0361f48 Lineup sidebar badges 2022-04-07 06:54:24 +00:00
Philip Allgaier
cfa048ea4e Only show "required" indicator if we have a selector label (#12241) 2022-04-06 22:11:12 +00:00
Joakim Sørensen
bbca7b762b Use selectors for add-on network configuration (#12235)
* Use selectors for add-on network configuration

* Show container port as UOM if advanced user

* adjust
2022-04-06 22:21:06 +02:00
Erik Montnemery
1dba849567 Fix statistics chart for sum stat without state (#12238) 2022-04-06 20:54:11 +02:00
Marius
aff1ec10bf replace ToggleSwitch with new LightSwitch (#12218) 2022-04-06 16:26:34 +02:00
Joakim Sørensen
351ec08a71 Use selectors for add-on configurations (#12234) 2022-04-06 09:57:17 +02:00
Paulus Schoutsen
a1a6a2cd30 Bumped version to 20220405.0 2022-04-05 15:49:13 -07:00
Joakim Sørensen
4e82c23b29 Fixes for flow help icon (#12224) 2022-04-05 15:47:24 -07:00
Paulus Schoutsen
59595aabde Add helpers to all selectors (#12230) 2022-04-05 15:26:52 -05:00
Paulus Schoutsen
358f91c2a9 Add statistic name to adjust dialog (#12229) 2022-04-05 17:25:23 +00:00
Joakim Sørensen
e0e01e68b4 Use change instead of click when selecting home assistant in backup (#12226) 2022-04-05 10:48:45 -05:00
Paulus Schoutsen
61dc4eaaea Throttle counting updates (#12223) 2022-04-05 09:55:01 +02:00
Paulus Schoutsen
65c4d02452 Fix Safari dates (#12222) 2022-04-04 21:54:35 -07:00
Philip Allgaier
f78ce2c844 Sort "Switch as" domains and add separator (#12216) 2022-04-04 18:47:26 -05:00
Philip Allgaier
4d1ab83b30 Hide "Show as" separator if there is nothing above/below it (#12219) 2022-04-04 18:44:21 -05:00
Zack Barett
fb4b40b828 Fix Entity Settings missing (#12221) 2022-04-04 22:49:11 +00:00
Philip Allgaier
db0c4ef941 Fix "Show as" in entity registry (#12215) 2022-04-04 14:16:03 -05:00
Philip Allgaier
c5b60b826b Enable <ha-form> overflow overriding via part (#12204) 2022-04-04 08:41:12 -05:00
Philip Allgaier
718f0330a7 Fix button card behavior to toggle scenes (#12203) 2022-04-04 08:40:10 -05:00
Philip Allgaier
89e31486c5 Ensure history entity picker wraps to next line on mobile (#12201) 2022-04-04 08:39:45 -05:00
Philip Allgaier
717eec1860 Allow "none" again as secondary entity information (#12199) 2022-04-04 08:39:28 -05:00
Philip Allgaier
b6e51352e3 Fan more-info: Add margin to sections (#12202) 2022-04-04 08:38:53 -05:00
Joakim Sørensen
2ade728bc3 Make the progress bar not jump the dialog (#12212) 2022-04-04 08:20:37 +02:00
Joakim Sørensen
62f227da83 Use installed_version for update entities (#12194) 2022-04-01 19:28:39 +02:00
Bram Kragten
9557b604da Bumped version to 20220401.0 2022-04-01 18:35:08 +02:00
Zack Barett
b45c355c9f Fix for Mult enabled selectors when required (#12191) 2022-04-01 18:34:32 +02:00
Joakim Sørensen
0b47d2c687 Redirect old backup links to backup integration for non supervised (#12183) 2022-04-01 08:05:53 -07:00
Joakim Sørensen
8baa0b2a9b Hide skip when auto_update is true for updates (#12184) 2022-04-01 14:37:47 +02:00
Joakim Sørensen
c68a1d21ff Do not offer to partially backup homeassistant configuration (#12188) 2022-04-01 14:37:18 +02:00
Paulus Schoutsen
419d659311 Guard calling input select row with bad option (#12181) 2022-03-31 20:32:10 -05:00
Bram Kragten
ba8b20d877 Fix url config when not logged in to cloud (#12176) 2022-03-31 10:59:44 -05:00
Bram Kragten
8de542388f Show hidden entities on device page (#12177) 2022-03-31 10:59:20 -05:00
Raman Gupta
e6c580aadc Add zwave-js node alerts to device configuration page (#12173)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-03-31 09:06:47 -05:00
Joakim Sørensen
11696c566a Add support for my backup links (#12172) 2022-03-31 09:05:39 -05:00
Joakim Sørensen
edc15940a2 Add icon to add-on picker (#12174) 2022-03-31 15:16:19 +02:00
Joakim Sørensen
bf35ee549d Only use the state to mark updates as pending (#12171) 2022-03-31 15:15:21 +02:00
Philip Allgaier
4c3baa678c Bump caniuse-lite (#12168) 2022-03-30 17:12:22 -05:00
Philip Allgaier
0bb2767696 Add missing labels to media player volume buttons (#12170) 2022-03-30 17:11:50 -05:00
Philip Allgaier
80a4852325 Add missing label for new statistics "adjust sum" button (#12169) 2022-03-30 17:11:23 -05:00
Bram Kragten
9c3e0fc997 Merge branch 'master' into dev 2022-03-30 20:48:37 +02:00
Zack Barett
9e4bee123f Bumped version to 20220330.0 (#12164) 2022-03-30 11:33:59 -07:00
Bram Kragten
c2c09b1284 Add switch as x to entity settings (#12161)
Co-authored-by: Zack <zackbarett@hey.com>
2022-03-30 18:19:26 +00:00
Zack Barett
bad776b979 Allow Sensor Units to be updated via Entity Registry (#12143) 2022-03-30 13:03:19 -05:00
Zack Barett
396791b805 Fix for Mobile View of Entities Table (#12160) 2022-03-30 19:52:05 +02:00
Joakim Sørensen
2b1457e1cd Add panel to Backup integration (#11671)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-30 17:22:15 +00:00
NachtaktiverHalbaffe
b5861869e3 Add shuffle and repeat-mode of media_player to UI (#12052)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-30 17:16:27 +00:00
Zack Barett
9444228907 List Selector (#12099)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-03-30 09:14:39 -07:00
Bram Kragten
86afd883a5 Add helpers to list when searching in add integration (#12159) 2022-03-30 13:55:54 +00:00
Bram Kragten
062f21aa91 Add options to selectors gallery (#12156) 2022-03-30 12:34:37 +00:00
Joakim Sørensen
ba235ac797 Import components that are allowed to be defined in markdown (#12158) 2022-03-30 14:22:24 +02:00
Bram Kragten
505c22248b Use brand icon instead of domain icon for helpers (#12157) 2022-03-30 13:53:37 +02:00
Bram Kragten
624cb48f78 Add support for my links to create a helper config entry (#12155) 2022-03-30 13:53:28 +02:00
Zack Barett
7ab54ee5ce Update Pickers and selectors with required (#12151)
* Update Pickers and selectors with required

* Use native * for device and entity
2022-03-30 06:48:56 -05:00
blair
f5af63a50e Automation description text overflow (#12040)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-03-30 10:22:26 +00:00
Marius
ff80ab34ee Allow device_tracker entities to use state_color (#12127) 2022-03-30 11:35:37 +02:00
Joakim Sørensen
cfc1999a28 Allow ha-alert to be used in our markdown render (#12153) 2022-03-30 11:22:36 +02:00
Joakim Sørensen
7ca28469b7 Fix theme settings on design page (#12154) 2022-03-30 11:21:00 +02:00
Raman Gupta
ac670614b4 Add support for new timer properties (#11940) 2022-03-30 00:00:36 -07:00
Joakim Sørensen
e263b57296 Fetch release notes for update entities that provides it (#12148)
* Fetch release notes for update entities that provides it

* lint
2022-03-30 07:22:12 +02:00
Paulus Schoutsen
c7050e4676 Update adjust statistic dialog (#12118)
* Update text for adjust statistic dialog

* Change everything

* Import type

* Max show 5

* Revert back the API change

* Hide adjust button if no sum

* Adjustments

* Update src/panels/developer-tools/statistics/developer-tools-statistics.ts

* Render optional

Co-authored-by: Zack <zackbarett@hey.com>
2022-03-29 20:40:17 -07:00
Paulus Schoutsen
00cbd1d9e6 Add entity source API (#12149) 2022-03-29 17:09:51 -07:00
Zack Barett
2a12172eeb Bumped version to 20220329.0 (#12152) 2022-03-29 22:25:56 +00:00
Joakim Sørensen
85d3011625 Add badge to configuration sidebar to indicate pending updates (#12146) 2022-03-29 07:35:05 -05:00
Zack Barett
ca22ec6340 Add selector initial values (#12142) 2022-03-28 10:38:58 -07:00
Zack Barett
61f6e8855b Allow binary sensor device class updates (#12124) 2022-03-28 10:44:21 -05:00
Bram Kragten
a44b8981e1 break theme picker out of lovelace (#12140) 2022-03-28 08:21:16 -07:00
Zack Barett
b080bca9ce Add Area Multiple Selector option (#12138) 2022-03-28 09:07:00 -05:00
Bram Kragten
d30e8ee9d8 Make padding on settings row content consistent (#12139) 2022-03-28 08:50:07 -05:00
Bram Kragten
637e4203e5 Fix z-index map, always set icon for location selector (#12137) 2022-03-28 13:14:24 +00:00
Zack Barett
2648a53bbc Merge pull request #12135 from home-assistant/update-automation-type 2022-03-28 07:43:21 -05:00
Paulus Schoutsen
b3fa0cccb4 Add variables to automation trigger type 2022-03-27 20:33:22 -07:00
Zack Barett
dd963be723 Add Day to duration selector (#12125) 2022-03-24 17:57:20 -07:00
Paulus Schoutsen
224df896a1 Allow rendering helper text from strings.json (#12119)
* Allow rendering helper text from strings.json

* Persistent helpers

* Update src/components/ha-base-time-input.ts

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Update src/components/ha-base-time-input.ts

Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-24 17:50:38 -07:00
Pawel
a58b4fb262 Fix possibility to enable entity disabled by integration (#12121)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-24 20:10:49 +01:00
Brynley McDonald
27ca61ec85 Fix issue where theme select does not appear when user's theme is deleted (#12104) 2022-03-24 16:31:55 +01:00
Zack Barett
859f49f3eb Update type for backend (#12122) 2022-03-24 13:47:07 +00:00
Erik Montnemery
40d878689f Sort selectors (#12120) 2022-03-24 11:53:32 +01:00
Zack Barett
420e8fe1ff Merge pull request #12116 from home-assistant/docs-only-form 2022-03-23 17:40:40 -05:00
Erik Montnemery
df96199433 Support descriptions in flow menu steps (#12108)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-23 22:14:57 +00:00
Erik Montnemery
f493280f0a Exclude restored automations from dashboard (#12113) 2022-03-23 15:05:41 -07:00
Paulus Schoutsen
cbd030a379 Only show docs link when showing a form 2022-03-23 14:53:02 -07:00
Zack Barett
95b80accc9 Merge pull request #12085 from goyney/update-mdi-to-6-6-95 2022-03-23 15:17:31 -05:00
Erik Montnemery
c522670815 Fix loading traces for automation with custom id (#12112) 2022-03-23 13:11:46 -07:00
Joakim Sørensen
7b6d3c0e36 Use update entities for showing updates on configuration panel (#12100)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-23 13:04:29 -07:00
Michael Irigoyen
504b043159 Update lock file with MDI updates 2022-03-23 08:46:22 -05:00
Michael Irigoyen
dffc66ccc3 Merge branch 'dev' of github.com:goyney/frontend into update-mdi-to-6-6-95 2022-03-23 08:43:46 -05:00
Michael Irigoyen
f5f8be8276 Update required version of MDI to 6.6.95 2022-03-20 23:32:21 -05:00
Zack Barett
f9ccfa00a2 Merge pull request #12045 from home-assistant/hot-fix-03142022 2022-03-14 11:31:11 -05:00
Zack Barett
070e11a2db Fix @changed where using ev.detail (#12043) 2022-03-14 11:13:05 -05:00
Zack Barett
fc1c6cea24 Fix: Changing Blueprint Automation Name (#12036) 2022-03-14 10:35:18 -05:00
Zack
c6a103bd30 Minor Version whoops 2022-03-14 10:27:59 -05:00
Zack
9d1618024e Bumped version to 20220314.0 2022-03-14 07:52:06 -05:00
Zack Barett
307aa161a6 Script ID update with Alias (#12008) 2022-03-14 07:46:40 -05:00
Zack Barett
affa6a92e7 Fix: Allow for deleting Input_select options (#12007) 2022-03-14 07:46:40 -05:00
Zack Barett
1e5ec241d5 Fix For Selecting Device Class (#12010) 2022-03-14 07:46:40 -05:00
Zack Barett
27a98a32fc Fix Dashboard Editing (#12011) 2022-03-14 07:46:40 -05:00
Zack Barett
2c8ac58f97 Fix changing cost number in energy settings (#12009) 2022-03-14 07:46:40 -05:00
Charles Garwood
6b995969b1 Fix zwave_js set config dropdown default value (#11974)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-03-14 07:46:40 -05:00
Raman Gupta
72c107484a Fix zwave_js 'add/remove device' disabled bug (#12000)
* Fix zwave_js 'add/remove device' disabled bug

* revert extra change
2022-03-14 07:46:39 -05:00
Bram Kragten
d1085b6657 Fix theme setting (#11977) 2022-03-14 07:45:23 -05:00
250 changed files with 9410 additions and 3532 deletions

View File

@@ -3,10 +3,10 @@ const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const bundle = require("./bundle.js");
const log = require("fancy-log");
const WebpackBar = require("webpackbar");
const paths = require("./paths.js");
const bundle = require("./bundle.js");
class LogStartCompilePlugin {
ignoredFirst = false;
@@ -138,6 +138,8 @@ const createWebpackConfig = ({
"lit/directives/cache$": "lit/directives/cache.js",
"lit/directives/repeat$": "lit/directives/repeat.js",
"lit/polyfill-support$": "lit/polyfill-support.js",
"@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js",
},
},
output: {

View File

@@ -31,7 +31,7 @@ export const mockHassioSupervisor = (hass: MockHomeAssistant) => {
version_latest: "3.6.2",
update_available: false,
repository: "a0d7b954",
icon: true,
icon: false,
logo: true,
},
{

Binary file not shown.

View File

@@ -53,13 +53,19 @@ class DemoBlackWhiteRow extends LitElement {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
});
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
handleSubmit(ev) {

11
gallery/src/data/text.ts Normal file
View File

@@ -0,0 +1,11 @@
export const LONG_TEXT = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum.
Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci.
Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo.
In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla.
Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim.
`;

View File

@@ -8,7 +8,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import type { Condition } from "../../../../src/data/automation";
import type { ConditionWithShorthand } from "../../../../src/data/automation";
import "../../../../src/panels/config/automation/condition/ha-automation-condition";
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
@@ -20,7 +20,7 @@ import { HaTimeCondition } from "../../../../src/panels/config/automation/condit
import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
const SCHEMAS: { name: string; conditions: Condition[] }[] = [
const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
{
name: "State",
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
@@ -69,6 +69,14 @@ const SCHEMAS: { name: string; conditions: Condition[] }[] = [
name: "Trigger",
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
},
{
name: "Shorthand",
conditions: [
{ and: HaLogicalCondition.defaultConfig.conditions },
{ or: HaLogicalCondition.defaultConfig.conditions },
{ not: HaLogicalCondition.defaultConfig.conditions },
],
},
];
@customElement("demo-automation-editor-condition")

View File

@@ -159,13 +159,19 @@ export class DemoHaAlert extends LitElement {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
});
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static get styles() {

View File

@@ -3,18 +3,7 @@ import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-faded";
import "../../../../src/components/ha-markdown";
const LONG_TEXT = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum.
Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci.
Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo.
In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla.
Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim.
`;
import { LONG_TEXT } from "../../data/text";
const SMALL_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";

View File

@@ -1,18 +1,18 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, html } from "lit";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-form/ha-form";
import "../../components/demo-black-white-row";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
import { getEntity } from "../../../../src/fake_data/entity";
import "../../components/demo-black-white-row";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
@@ -147,7 +147,9 @@ const SCHEMAS: {
{ name: "target", selector: { target: {} } },
{ name: "number", selector: { number: { min: 0, max: 10 } } },
{ name: "boolean", selector: { boolean: {} } },
{ name: "time", selector: { time: {} } },
{ name: "time", required: true, selector: { time: {} } },
{ name: "datetime", required: true, selector: { datetime: {} } },
{ name: "date", required: true, selector: { date: {} } },
{ name: "action", selector: { action: {} } },
{ name: "text", selector: { text: { multiline: false } } },
{ name: "text_multiline", selector: { text: { multiline: true } } },

View File

@@ -1,20 +1,20 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, css, html } from "lit";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import { BlueprintInput } from "../../../../src/data/blueprint";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row";
import { BlueprintInput } from "../../../../src/data/blueprint";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { getEntity } from "../../../../src/fake_data/entity";
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
@@ -109,7 +109,7 @@ const AREAS = [
const SCHEMAS: {
name: string;
input: Record<string, BlueprintInput | null>;
input: Record<string, (BlueprintInput & { required?: boolean }) | null>;
}[] = [
{
name: "One of each",
@@ -166,8 +166,11 @@ const SCHEMAS: {
object: { name: "Object", selector: { object: {} } },
select_radio: {
name: "Select (Radio)",
selector: { select: { options: ["Option 1", "Option 2"] } },
selector: {
select: { options: ["Option 1", "Option 2"], mode: "list" },
},
},
template: { name: "Template", selector: { template: {} } },
select: {
name: "Select",
selector: {
@@ -183,6 +186,22 @@ const SCHEMAS: {
},
},
},
select_custom: {
name: "Select (Custom)",
selector: {
select: {
custom_value: true,
options: [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
"Option 6",
],
},
},
},
icon: { name: "Icon", selector: { icon: {} } },
media: { name: "Media", selector: { media: {} } },
location: { name: "Location", selector: { location: {} } },
@@ -202,6 +221,35 @@ const SCHEMAS: {
input: {
entity: { name: "Entity", selector: { entity: { multiple: true } } },
device: { name: "Device", selector: { device: { multiple: true } } },
area: { name: "Area", selector: { area: { multiple: true } } },
select: {
name: "Select Multiple",
selector: {
select: {
multiple: true,
custom_value: true,
options: [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
"Option 6",
],
},
},
},
select_checkbox: {
name: "Select Multiple (Checkbox)",
required: false,
selector: {
select: {
mode: "list",
multiple: true,
options: ["Option 1", "Option 2", "Option 3", "Option 4"],
},
},
},
},
},
];
@@ -210,6 +258,14 @@ const SCHEMAS: {
class DemoHaSelector extends LitElement implements ProvideHassElement {
@state() public hass!: HomeAssistant;
@state() private _disabled = false;
@state() private _required = false;
@state() private _helper = false;
@state() private _label = true;
private data = SCHEMAS.map(() => ({}));
constructor() {
@@ -343,6 +399,36 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
protected render(): TemplateResult {
return html`
<div class="options">
<ha-formfield label="Labels">
<ha-switch
.name=${"label"}
.checked=${this._label}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
<ha-formfield label="Required">
<ha-switch
.name=${"required"}
.checked=${this._required}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
<ha-formfield label="Disabled">
<ha-switch
.name=${"disabled"}
.checked=${this._disabled}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
<ha-formfield label="Helper text">
<ha-switch
.name=${"helper"}
.checked=${this._helper}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
</div>
${SCHEMAS.map((info, idx) => {
const data = this.data[idx];
const valueChanged = (ev) => {
@@ -365,8 +451,12 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
.hass=${this.hass}
.selector=${value!.selector}
.key=${key}
.label=${this._label ? value!.name : undefined}
.value=${data[key] ?? value!.default}
.disabled=${this._disabled}
.required=${this._required}
@value-changed=${valueChanged}
.helper=${this._helper ? "Helper text" : undefined}
></ha-selector>
</ha-settings-row>
`
@@ -378,10 +468,21 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
`;
}
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}
static styles = css`
ha-selector {
width: 60;
}
.options {
max-width: 800px;
margin: 16px auto;
}
.options ha-formfield {
margin-right: 16px;
}
`;
}

View File

@@ -0,0 +1,3 @@
---
title: Tips
---

View File

@@ -0,0 +1,73 @@
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-tip";
import "../../../../src/components/ha-card";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
const tips: (string | TemplateResult)[] = [
"Test tip",
"Bigger test tip, with some random text just to fill up as much space as possible without it looking like I'm really trying to to that",
html`<i>Tip</i> <b>with</b> <sub>HTML</sub>`,
];
@customElement("demo-components-ha-tip")
export class DemoHaTip extends LitElement {
protected render(): TemplateResult {
return html` ${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-tip ${mode} demo">
<div class="card-content">
${tips.map((tip) => html`<ha-tip>${tip}</ha-tip>`)}
</div>
</ha-card>
</div>
`
)}`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static get styles() {
return css`
:host {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
ha-tip {
margin-bottom: 14px;
}
ha-card {
margin: 24px auto;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-tip": DemoHaTip;
}
}

View File

@@ -9,7 +9,7 @@ const CONFIGS = [
heading: "markdown-it demo",
config: `
- type: markdown
content: >
content: >-
# h1 Heading 8-)
## h2 Heading
@@ -249,6 +249,17 @@ const CONFIGS = [
::: warning
*here be dragons*
:::
### ha-alert
You can use our [\`ha-alert\`](https://design.home-assistant.io/#components/ha-alert) component in markdown content rendered in the Home Assistant Frontend.
<ha-alert alert-type="error">This is an error alert — check it out!</ha-alert>
<ha-alert alert-type="warning">This is a warning alert — check it out!</ha-alert>
<ha-alert alert-type="info">This is an info alert — check it out!</ha-alert>
<ha-alert alert-type="success">This is a success alert — check it out!</ha-alert>
<ha-alert title="Test alert">This is an alert with a title</ha-alert>
`,
},
];

View File

@@ -5,6 +5,7 @@ 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";
@@ -13,10 +14,11 @@ import {
provideHass,
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
import { LONG_TEXT } from "../../data/text";
const base_attributes = {
title: "Awesome",
current_version: "1.2.2",
installed_version: "1.2.2",
latest_version: "1.2.3",
release_url: "https://home-assistant.io",
supported_features: UPDATE_SUPPORT_INSTALL,
@@ -48,7 +50,7 @@ const ENTITIES = [
}),
getEntity("update", "update5", "off", {
...base_attributes,
current_version: "1.2.3",
installed_version: "1.2.3",
friendly_name: "No update",
}),
getEntity("update", "update6", "off", {
@@ -100,14 +102,43 @@ const ENTITIES = [
}),
getEntity("update", "update14", "off", {
...base_attributes,
current_version: null,
friendly_name: "Update without current_version",
installed_version: null,
friendly_name: "Update without installed_version",
}),
getEntity("update", "update15", "off", {
...base_attributes,
latest_version: null,
friendly_name: "Update without latest_version",
}),
getEntity("update", "update16", "off", {
...base_attributes,
friendly_name: "Update with release notes",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}),
getEntity("update", "update17", "off", {
...base_attributes,
friendly_name: "Update with release notes error",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}),
getEntity("update", "update18", "off", {
...base_attributes,
friendly_name: "Update with release notes loading",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}),
getEntity("update", "update19", "on", {
...base_attributes,
friendly_name: "Update with auto update",
auto_update: true,
}),
getEntity("update", "update20", "on", {
...base_attributes,
in_progress: true,
title: undefined,
friendly_name: "Installing without title",
}),
];
@customElement("demo-more-info-update")
@@ -130,6 +161,24 @@ class DemoMoreInfoUpdate extends LitElement {
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
hass.mockWS(
"update/release_notes",
(msg: { type: string; entity_id: string }) => {
if (msg.entity_id === "update.update16") {
return LONG_TEXT;
}
if (msg.entity_id === "update.update17") {
return Promise.reject({
code: "error",
message: "Could not fetch release notes",
});
}
if (msg.entity_id === "update.update18") {
return undefined;
}
return null;
}
);
}
}

View File

@@ -39,7 +39,14 @@ import type { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"];
const SUPPORTED_UI_TYPES = [
"string",
"select",
"boolean",
"integer",
"float",
"schema",
];
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
new Type("!secret", {
@@ -48,6 +55,8 @@ const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
}),
]);
const MASKED_FIELDS = ["password", "secret", "token"];
@customElement("hassio-addon-config")
class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -75,19 +84,66 @@ class HassioAddonConfig extends LitElement {
public computeLabel = (entry: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.name ||
this.addon.translations.en?.configuration?.[entry.name].name ||
this.addon.translations.en?.configuration?.[entry.name]?.name ||
entry.name;
private _schema = memoizeOne((schema: HaFormSchema[]): HaFormSchema[] =>
// @ts-expect-error supervisor does not implement [string, string] for select.options[]
schema.map((entry) =>
entry.type === "select"
? {
...entry,
options: entry.options.map((option) => [option, option]),
}
: entry
)
public computeHelper = (entry: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.description ||
this.addon.translations.en?.configuration?.[entry.name]?.description ||
"";
private _convertSchema = memoizeOne(
// Convert supervisor schema to selectors
(schema: Record<string, any>): HaFormSchema[] =>
schema.map((entry) =>
entry.type === "select"
? {
name: entry.name,
required: entry.required,
selector: { select: { options: entry.options } },
}
: entry.type === "string"
? entry.multiple
? {
name: entry.name,
required: entry.required,
selector: {
select: { options: [], multiple: true, custom_value: true },
},
}
: {
name: entry.name,
required: entry.required,
selector: {
text: {
type:
entry.format || MASKED_FIELDS.includes(entry.name)
? "password"
: "text",
},
},
}
: entry.type === "boolean"
? {
name: entry.name,
required: entry.required,
selector: { boolean: {} },
}
: entry.type === "schema"
? {
name: entry.name,
required: entry.required,
selector: { object: {} },
}
: entry.type === "float" || entry.type === "integer"
? {
name: entry.name,
required: entry.required,
selector: { number: { mode: "box" } },
}
: entry
)
);
private _filteredShchema = memoizeOne(
@@ -140,7 +196,8 @@ class HassioAddonConfig extends LitElement {
.data=${this._options!}
@value-changed=${this._configChanged}
.computeLabel=${this.computeLabel}
.schema=${this._schema(
.computeHelper=${this.computeHelper}
.schema=${this._convertSchema(
this._showOptional
? this.addon.schema!
: this._filteredShchema(
@@ -197,8 +254,9 @@ class HassioAddonConfig extends LitElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._canShowSchema = !this.addon.schema!.find(
// @ts-ignore
(entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple
(entry) =>
// @ts-ignore
!SUPPORTED_UI_TYPES.includes(entry.type)
);
this._yamlMode = !this._canShowSchema;
}

View File

@@ -1,4 +1,3 @@
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResultGroup,
@@ -8,10 +7,13 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
@@ -24,16 +26,6 @@ import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
interface NetworkItem {
description: string;
container: string;
host: number | null;
}
interface NetworkItemInput extends PaperInputElement {
container: string;
}
@customElement("hassio-addon-network")
class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -42,9 +34,13 @@ class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@state() private _showOptional = false;
@state() private _configHasChanged = false;
@state() private _error?: string;
@state() private _config?: NetworkItem[];
@state() private _config?: Record<string, any>;
public connectedCallback(): void {
super.connectedCallback();
@@ -56,6 +52,10 @@ class HassioAddonNetwork extends LitElement {
return html``;
}
const hasHiddenOptions = Object.keys(this._config).find(
(entry) => this._config![entry] === null
);
return html`
<ha-card
.header=${this.supervisor.localize(
@@ -63,52 +63,49 @@ class HassioAddonNetwork extends LitElement {
)}
>
<div class="card-content">
<p>
${this.supervisor.localize(
"addon.configuration.network.introduction"
)}
</p>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<table>
<tbody>
<tr>
<th>
${this.supervisor.localize(
"addon.configuration.network.container"
)}
</th>
<th>
${this.supervisor.localize(
"addon.configuration.network.host"
)}
</th>
<th>${this.supervisor.localize("common.description")}</th>
</tr>
${this._config!.map(
(item) => html`
<tr>
<td>${item.container}</td>
<td>
<paper-input
@value-changed=${this._configChanged}
placeholder=${this.supervisor.localize(
"addon.configuration.network.disabled"
)}
.value=${item.host ? String(item.host) : ""}
.container=${item.container}
no-label-float
></paper-input>
</td>
<td>${this._computeDescription(item)}</td>
</tr>
`
)}
</tbody>
</table>
<ha-form
.data=${this._config}
@value-changed=${this._configChanged}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
.schema=${this._createSchema(
this._config,
this._showOptional,
this.hass.userData?.showAdvanced || false
)}
></ha-form>
</div>
${hasHiddenOptions
? html`<ha-formfield
class="show-optional"
.label=${this.supervisor.localize(
"addon.configuration.network.show_disabled"
)}
>
<ha-switch
@change=${this._toggleOptional}
.checked=${this._showOptional}
>
</ha-switch>
</ha-formfield>`
: ""}
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
${this.supervisor.localize("common.reset_defaults")}
</ha-progress-button>
<ha-progress-button @click=${this._saveTapped}>
<ha-progress-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged}
>
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
@@ -123,50 +120,60 @@ class HassioAddonNetwork extends LitElement {
}
}
private _computeDescription = (item: NetworkItem): string =>
this.addon.translations[this.hass.language]?.network?.[item.container]
?.description ||
this.addon.translations.en?.network?.[item.container]?.description ||
item.description;
private _createSchema = memoizeOne(
(
config: Record<string, number>,
showOptional: boolean,
advanced: boolean
): HaFormSchema[] =>
(showOptional
? Object.keys(config)
: Object.keys(config).filter((entry) => config[entry] !== null)
).map((entry) => ({
name: entry,
selector: {
number: {
mode: "box",
min: 0,
max: 65535,
unit_of_measurement: advanced ? entry : undefined,
},
},
}))
);
private _computeLabel = (_: HaFormSchema): string => "";
private _computeHelper = (item: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.network?.[item.name] ||
this.addon.translations.en?.network?.[item.name] ||
this.addon.network_description?.[item.name] ||
item.name;
private _setNetworkConfig(): void {
const network = this.addon.network || {};
const description = this.addon.network_description || {};
const items: NetworkItem[] = Object.keys(network).map((key) => ({
container: key,
host: network[key],
description: description[key],
}));
this._config = items.sort((a, b) => (a.container > b.container ? 1 : -1));
this._config = this.addon.network || {};
}
private async _configChanged(ev: Event): Promise<void> {
const target = ev.target as NetworkItemInput;
this._config!.forEach((item) => {
if (
item.container === target.container &&
item.host !== parseInt(String(target.value), 10)
) {
item.host = target.value ? parseInt(String(target.value), 10) : null;
}
});
private async _configChanged(ev: CustomEvent): Promise<void> {
this._configHasChanged = true;
this._config! = ev.detail.value;
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const data: HassioAddonSetOptionParams = {
network: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "option",
};
button.actionSuccess();
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
@@ -177,19 +184,21 @@ class HassioAddonNetwork extends LitElement {
"error",
extractApiErrorMessage(err)
);
button.actionError();
}
}
button.progress = false;
private _toggleOptional() {
this._showOptional = !this._showOptional;
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
this._error = undefined;
const networkconfiguration = {};
this._config!.forEach((item) => {
networkconfiguration[item.container] = parseInt(String(item.host), 10);
Object.entries(this._config!).forEach(([key, value]) => {
networkconfiguration[key] = value ?? null;
});
const data: HassioAddonSetOptionParams = {
@@ -198,11 +207,13 @@ class HassioAddonNetwork extends LitElement {
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "option",
};
button.actionSuccess();
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
@@ -213,8 +224,8 @@ class HassioAddonNetwork extends LitElement {
"error",
extractApiErrorMessage(err)
);
button.actionError();
}
button.progress = false;
}
static get styles(): CSSResultGroup {
@@ -232,6 +243,9 @@ class HassioAddonNetwork extends LitElement {
display: flex;
justify-content: space-between;
}
.show-optional {
padding: 16px;
}
`,
];
}

View File

@@ -32,13 +32,6 @@ interface AddonCheckboxItem extends CheckboxItem {
const _computeFolders = (folders): CheckboxItem[] => {
const list: CheckboxItem[] = [];
if (folders.includes("homeassistant")) {
list.push({
slug: "homeassistant",
name: "Home Assistant configuration",
checked: false,
});
}
if (folders.includes("ssl")) {
list.push({ slug: "ssl", name: "SSL", checked: false });
}
@@ -100,7 +93,7 @@ export class SupervisorBackupContent extends LitElement {
this.folders = _computeFolders(
this.backup
? this.backup.folders
: ["homeassistant", "ssl", "share", "media", "addons/local"]
: ["ssl", "share", "media", "addons/local"]
);
this.addons = _computeAddons(
this.backup ? this.backup.addons : this.supervisor?.supervisor.addons
@@ -187,7 +180,7 @@ export class SupervisorBackupContent extends LitElement {
>
<ha-checkbox
.checked=${this.homeAssistant}
@click=${this.toggleHomeAssistant}
@change=${this.toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>

View File

@@ -10,6 +10,7 @@ import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addons";
import "./hassio-update";
import "../../../src/layouts/hass-subpage";
@customElement("hassio-dashboard")
class HassioDashboard extends LitElement {
@@ -22,6 +23,31 @@ class HassioDashboard extends LitElement {
@property({ attribute: false }) public route!: Route;
protected render(): TemplateResult {
if (atLeastVersion(this.hass.config.version, 2022, 5)) {
return html`<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.header=${this.supervisor.localize("panel.addons")}
>
<hassio-addons
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-addons>
<a href="/hassio/store">
<ha-fab
.label=${this.supervisor.localize("panel.store")}
extended
class="non-tabs"
>
<ha-svg-icon
slot="icon"
.path=${mdiStorePlus}
></ha-svg-icon> </ha-fab
></a>
</hass-subpage>`;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
@@ -74,6 +100,12 @@ class HassioDashboard extends LitElement {
.content {
margin: 0 auto;
}
ha-fab.non-tabs {
position: fixed;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
`,
];
}

View File

@@ -8,24 +8,27 @@ import { atLeastVersion } from "../../src/common/config/version";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../src/types";
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [
{
translationKey: atLeastVersion(hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: atLeastVersion(hass.config.version, 2021, 12)
? mdiPuzzle
: mdiViewDashboard,
},
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
{
translationKey: "panel.system",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
atLeastVersion(hass.config.version, 2022, 5)
? []
: [
{
translationKey: atLeastVersion(hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: atLeastVersion(hass.config.version, 2021, 12)
? mdiPuzzle
: mdiViewDashboard,
},
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
{
translationKey: "panel.system",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];

View File

@@ -45,7 +45,6 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
@@ -55,6 +54,12 @@ declare global {
}
}
const SUPERVISOR_UPDATE_NAMES = {
core: "Home Assistant Core",
os: "Home Assistant Operating System",
supervisor: "Home Assistant Supervisor",
};
type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = (

View File

@@ -72,8 +72,8 @@
"@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.5.95",
"@mdi/svg": "6.5.95",
"@mdi/js": "6.6.95",
"@mdi/svg": "6.6.95",
"@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",
@@ -108,7 +108,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.1.5",
"home-assistant-js-websocket": "^7.0.1",
"home-assistant-js-websocket": "^7.0.3",
"idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0",

View File

@@ -15,7 +15,7 @@ if [ -z $(which hass) ]; then
echo "Installing Home Asstant core from dev."
python3 -m pip install --upgrade \
colorlog \
git+git://github.com/home-assistant/home-assistant.git@dev
git+https://github.com/home-assistant/home-assistant.git@dev
fi
if [ ! -d "${WD}/config" ]; then

View File

@@ -1,6 +1,6 @@
[metadata]
name = home-assistant-frontend
version = 20220322.0
version = 20220425.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -12,7 +12,7 @@ export const isNavigationClick = (e: MouseEvent) => {
const anchor = e
.composedPath()
.filter((n) => (n as HTMLElement).tagName === "A")[0] as
.find((n) => (n as HTMLElement).tagName === "A") as
| HTMLAnchorElement
| undefined;
if (

View File

@@ -29,8 +29,11 @@ import {
mdiPowerPlug,
mdiPowerPlugOff,
mdiRadioboxBlank,
mdiSmoke,
mdiSnowflake,
mdiSmokeDetector,
mdiSmokeDetectorAlert,
mdiSmokeDetectorVariant,
mdiSmokeDetectorVariantAlert,
mdiSquare,
mdiSquareOutline,
mdiStop,
@@ -52,6 +55,8 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
return is_off ? mdiBattery : mdiBatteryOutline;
case "battery_charging":
return is_off ? mdiBattery : mdiBatteryCharging;
case "carbon_monoxide":
return is_off ? mdiSmokeDetector : mdiSmokeDetectorAlert;
case "cold":
return is_off ? mdiThermometer : mdiSnowflake;
case "connectivity":
@@ -68,7 +73,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
case "tamper":
return is_off ? mdiCheckCircle : mdiAlertCircle;
case "smoke":
return is_off ? mdiCheckCircle : mdiSmoke;
return is_off ? mdiSmokeDetectorVariant : mdiSmokeDetectorVariantAlert;
case "heat":
return is_off ? mdiThermometer : mdiFire;
case "light":

View File

@@ -33,6 +33,7 @@ export const computeStateDisplay = (
return formatNumber(compareState, locale, {
style: "currency",
currency: stateObj.attributes.unit_of_measurement,
minimumFractionDigits: 2,
});
} catch (_err) {
// fallback to default

View File

@@ -8,26 +8,25 @@ import {
mdiCalendar,
mdiCast,
mdiCastConnected,
mdiCheckCircleOutline,
mdiClock,
mdiCloseCircleOutline,
mdiGestureTapButton,
mdiLanConnect,
mdiLanDisconnect,
mdiLightSwitch,
mdiLock,
mdiLockAlert,
mdiLockClock,
mdiLockOpen,
mdiPackage,
mdiPackageDown,
mdiPackageUp,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiToggleSwitchVariant,
mdiToggleSwitchVariantOff,
mdiWeatherNight,
mdiPackage,
mdiPackageDown,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { updateIsInstalling, UpdateEntity } from "../../data/update";
@@ -109,9 +108,11 @@ export const domainIcon = (
case "outlet":
return compareState === "on" ? mdiPowerPlug : mdiPowerPlugOff;
case "switch":
return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
return compareState === "on"
? mdiToggleSwitchVariant
: mdiToggleSwitchVariantOff;
default:
return mdiLightSwitch;
return mdiToggleSwitchVariant;
}
case "sensor": {

View File

@@ -7,6 +7,7 @@ export const iconColorCSS = css`
ha-state-icon[data-domain="calendar"][data-state="on"],
ha-state-icon[data-domain="camera"][data-state="streaming"],
ha-state-icon[data-domain="cover"][data-state="open"],
ha-state-icon[data-domain="device_tracker"][data-state="home"],
ha-state-icon[data-domain="fan"][data-state="on"],
ha-state-icon[data-domain="humidifier"][data-state="on"],
ha-state-icon[data-domain="light"][data-state="on"],

View File

@@ -1,4 +1,4 @@
export const promiseTimeout = (ms: number, promise: Promise<any>) => {
export const promiseTimeout = (ms: number, promise: Promise<any> | any) => {
const timeout = new Promise((_resolve, reject) => {
setTimeout(() => {
reject(`Timed out in ${ms} ms.`);

View File

@@ -0,0 +1,18 @@
import { HomeAssistant } from "../../types";
export const subscribePollingCollection = (
hass: HomeAssistant,
updateData: (hass: HomeAssistant) => void,
interval: number
) => {
let timeout;
const fetchData = async () => {
try {
await updateData(hass);
} finally {
timeout = setTimeout(() => fetchData(), interval);
}
};
fetchData();
return () => clearTimeout(timeout);
};

View File

@@ -0,0 +1,53 @@
import { HomeAssistant } from "../../types";
interface ResultCache<T> {
[entityId: string]: Promise<T> | undefined;
}
/**
* Call a function with result caching per entity.
* @param cacheKey key to store the cache on hass object
* @param cacheTime time to cache the results
* @param func function to fetch the data
* @param hass Home Assistant object
* @param entityId entity to fetch data for
* @param args extra arguments to pass to the function to fetch the data
* @returns
*/
export const timeCacheEntityPromiseFunc = async <T>(
cacheKey: string,
cacheTime: number,
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>,
hass: HomeAssistant,
entityId: string,
...args: any[]
): Promise<T> => {
let cache: ResultCache<T> | undefined = (hass as any)[cacheKey];
if (!cache) {
cache = hass[cacheKey] = {};
}
const lastResult = cache[entityId];
if (lastResult) {
return lastResult;
}
const result = func(hass, entityId, ...args);
cache[entityId] = result;
result.then(
// When successful, set timer to clear cache
() =>
setTimeout(() => {
cache![entityId] = undefined;
}, cacheTime),
// On failure, clear cache right away
() => {
cache![entityId] = undefined;
}
);
return result;
};

View File

@@ -1,43 +1,80 @@
import { HomeAssistant } from "../../types";
interface ResultCache<T> {
[entityId: string]: Promise<T> | undefined;
interface CacheResult<T> {
result: T;
cacheKey: any;
}
/**
* Caches a result of a promise for X time. Allows optional extra validation
* check to invalidate the cache.
* @param cacheKey the key to store the cache
* @param cacheTime the time to cache the result
* @param func the function to fetch the data
* @param generateCacheKey optional function to generate a cache key based on current hass + cached result. Cache is invalid if generates a different cache key.
* @param hass Home Assistant object
* @param args extra arguments to pass to the function to fetch the data
* @returns
*/
export const timeCachePromiseFunc = async <T>(
cacheKey: string,
cacheTime: number,
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>,
func: (hass: HomeAssistant, ...args: any[]) => Promise<T>,
generateCacheKey:
| ((hass: HomeAssistant, lastResult: T) => unknown)
| undefined,
hass: HomeAssistant,
entityId: string,
...args: any[]
): Promise<T> => {
let cache: ResultCache<T> | undefined = (hass as any)[cacheKey];
const anyHass = hass as any;
const lastResult: Promise<CacheResult<T>> | CacheResult<T> | undefined =
anyHass[cacheKey];
if (!cache) {
cache = hass[cacheKey] = {};
}
const checkCachedResult = (result: CacheResult<T>): T | Promise<T> => {
if (
!generateCacheKey ||
generateCacheKey(hass, result.result) === result.cacheKey
) {
return result.result;
}
const lastResult = cache[entityId];
anyHass[cacheKey] = undefined;
return timeCachePromiseFunc(
cacheKey,
cacheTime,
func,
generateCacheKey,
hass,
...args
);
};
// If we have a cached result, return it if it's still valid
if (lastResult) {
return lastResult;
return lastResult instanceof Promise
? lastResult.then(checkCachedResult)
: checkCachedResult(lastResult);
}
const result = func(hass, entityId, ...args);
cache[entityId] = result;
const resultPromise = func(hass, ...args);
anyHass[cacheKey] = resultPromise;
result.then(
resultPromise.then(
// When successful, set timer to clear cache
() =>
(result) => {
anyHass[cacheKey] = {
result,
cacheKey: generateCacheKey?.(hass, result),
};
setTimeout(() => {
cache![entityId] = undefined;
}, cacheTime),
anyHass[cacheKey] = undefined;
}, cacheTime);
},
// On failure, clear cache right away
() => {
cache![entityId] = undefined;
anyHass[cacheKey] = undefined;
}
);
return result;
return resultPromise;
};

View File

@@ -347,8 +347,8 @@ class StatisticsChart extends LitElement {
statTypes.forEach((type) => {
let val: number | null;
if (type === "sum") {
if (!initVal) {
initVal = val = stat.state;
if (initVal === null) {
initVal = val = stat.state || 0;
prevSum = stat.sum;
} else {
val = initVal + ((stat.sum || 0) - prevSum!);

View File

@@ -1,165 +1,167 @@
export const currencies = [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMK",
"ZWL",
];
export const createCurrencyListEl = () => {
const list = document.createElement("datalist");
list.id = "currencies";
for (const currency of [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMK",
"ZWL",
]) {
for (const currency of currencies) {
const option = document.createElement("option");
option.value = currency;
option.innerHTML = currency;

View File

@@ -52,6 +52,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public value?: string;
@property() public helper?: string;
@property() public devices?: DeviceRegistryEntry[];
@property() public areas?: AreaRegistryEntry[];
@@ -86,6 +88,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
@@ -267,8 +271,10 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
? this.hass.localize("ui.components.device-picker.device")
: this.label}
.value=${this._value}
.helper=${this.helper}
.renderer=${rowRenderer}
.disabled=${this.disabled}
.required=${this.required}
item-value-path="id"
item-label-path="name"
@opened-changed=${this._openedChanged}

View File

@@ -4,6 +4,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "./ha-device-picker";
import type { HaDevicePickerDeviceFilterFunc } from "./ha-device-picker";
@customElement("ha-devices-picker")
class HaDevicesPicker extends LitElement {
@@ -11,6 +12,12 @@ class HaDevicesPicker extends LitElement {
@property() public value?: string[];
@property() public helper?: string;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
/**
* Show entities from specific domains.
* @type {string}
@@ -35,6 +42,8 @@ class HaDevicesPicker extends LitElement {
@property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string;
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
@@ -49,11 +58,13 @@ class HaDevicesPicker extends LitElement {
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.deviceFilter=${this.deviceFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.value=${entityId}
.label=${this.pickedDeviceLabel}
.disabled=${this.disabled}
@value-changed=${this._deviceChanged}
></ha-device-picker>
</div>
@@ -61,11 +72,16 @@ class HaDevicesPicker extends LitElement {
)}
<div>
<ha-device-picker
allow-custom-entity
.hass=${this.hass}
.helper=${this.helper}
.deviceFilter=${this.deviceFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.label=${this.pickDeviceLabel}
.disabled=${this.disabled}
.required=${this.required && !currentDevices.length}
@value-changed=${this._addDevice}
></ha-device-picker>
</div>

View File

@@ -14,6 +14,12 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array }) public value?: string[];
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@property() public helper?: string;
/**
* Show entities from specific domains.
* @type {string}
@@ -92,6 +98,7 @@ class HaEntitiesPickerLight extends LitElement {
.entityFilter=${this._entityFilter}
.value=${entityId}
.label=${this.pickedEntityLabel}
.disabled=${this.disabled}
@value-changed=${this._entityChanged}
></ha-entity-picker>
</div>
@@ -99,6 +106,7 @@ class HaEntitiesPickerLight extends LitElement {
)}
<div>
<ha-entity-picker
allow-custom-entity
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
@@ -108,6 +116,9 @@ class HaEntitiesPickerLight extends LitElement {
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter}
.label=${this.pickEntityLabel}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !currentEntities.length}
@value-changed=${this._addEntity}
></ha-entity-picker>
</div>

View File

@@ -19,6 +19,8 @@ class HaEntityAttributePicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@@ -26,6 +28,8 @@ class HaEntityAttributePicker extends LitElement {
@property() public value?: string;
@property() public helper?: string;
@property({ type: Boolean }) private _opened = false;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@@ -61,6 +65,8 @@ class HaEntityAttributePicker extends LitElement {
"ui.components.entity.entity-attribute-picker.attribute"
)}
.disabled=${this.disabled || !this.entityId}
.required=${this.required}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomValue}
item-value-path="value"
item-label-path="label"

View File

@@ -39,6 +39,8 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@@ -46,6 +48,8 @@ export class HaEntityPicker extends LitElement {
@property() public value?: string;
@property() public helper?: string;
/**
* Show entities from specific domains.
* @type {Array}
@@ -302,9 +306,11 @@ export class HaEntityPicker extends LitElement {
.label=${this.label === undefined
? this.hass.localize("ui.components.entity.entity-picker.entity")
: this.label}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
.renderer=${rowRenderer}
.required=${this.required}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}

View File

@@ -13,9 +13,12 @@ import { HaComboBox } from "./ha-combo-box";
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (
item
) => html`<mwc-list-item twoline>
) => html`<mwc-list-item twoline graphic="icon">
<span>${item.name}</span>
<span slot="secondary">${item.slug}</span>
${item.icon
? html`<img slot="graphic" .src="/api/hassio/addons/${item.slug}/icon" />`
: ""}
</mwc-list-item>`;
@customElement("ha-addon-picker")
@@ -26,10 +29,14 @@ class HaAddonPicker extends LitElement {
@property() public value = "";
@property() public helper?: string;
@state() private _addons?: HassioAddonInfo[];
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@query("ha-combo-box") private _comboBox!: HaComboBox;
public open() {
@@ -55,6 +62,9 @@ class HaAddonPicker extends LitElement {
? this.hass.localize("ui.components.addon-picker.addon")
: this.label}
.value=${this._value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
.renderer=${rowRenderer}
.items=${this._addons}
item-value-path="slug"

View File

@@ -2,12 +2,12 @@ import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { Analytics, AnalyticsPreferences } from "../data/analytics";
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-checkbox";
import type { HaCheckbox } from "./ha-checkbox";
import type { HomeAssistant } from "../types";
import "./ha-settings-row";
import "./ha-switch";
import type { HaSwitch } from "./ha-switch";
const ADDITIONAL_PREFERENCES = [
{
@@ -40,62 +40,62 @@ export class HaAnalytics extends LitElement {
return html`
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${baseEnabled}
.preference=${"base"}
.disabled=${loading}
name="base"
>
</ha-checkbox>
</span>
<span slot="heading" data-for="base"> Basic analytics </span>
<span slot="description" data-for="base">
This includes information about your system.
</span>
<ha-switch
@change=${this._handleRowClick}
.checked=${baseEnabled}
.preference=${"base"}
.disabled=${loading}
name="base"
>
</ha-switch>
</ha-settings-row>
${ADDITIONAL_PREFERENCES.map(
(preference) =>
html`<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${this.analytics?.preferences[preference.key]}
.preference=${preference.key}
name=${preference.key}
>
</ha-checkbox>
${!baseEnabled
? html`<paper-tooltip animation-delay="0" position="right">
You need to enable basic analytics for this option to be
available
</paper-tooltip>`
: ""}
</span>
<span slot="heading" data-for=${preference.key}>
${preference.title}
</span>
<span slot="description" data-for=${preference.key}>
${preference.description}
</span>
</ha-settings-row>`
html`
<ha-settings-row>
<span slot="heading" data-for=${preference.key}>
${preference.title}
</span>
<span slot="description" data-for=${preference.key}>
${preference.description}
</span>
<span>
<ha-switch
@change=${this._handleRowClick}
.checked=${this.analytics?.preferences[preference.key]}
.preference=${preference.key}
name=${preference.key}
>
</ha-switch>
${!baseEnabled
? html`
<paper-tooltip animation-delay="0" position="right">
You need to enable basic analytics for this option to be
available
</paper-tooltip>
`
: ""}
</span>
</ha-settings-row>
`
)}
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${this.analytics?.preferences.diagnostics}
.preference=${"diagnostics"}
.disabled=${loading}
name="diagnostics"
>
</ha-checkbox>
</span>
<span slot="heading" data-for="diagnostics"> Diagnostics </span>
<span slot="description" data-for="diagnostics">
Share crash reports when unexpected errors occur.
</span>
<ha-switch
@change=${this._handleRowClick}
.checked=${this.analytics?.preferences.diagnostics}
.preference=${"diagnostics"}
.disabled=${loading}
name="diagnostics"
>
</ha-switch>
</ha-settings-row>
`;
}
@@ -120,23 +120,23 @@ export class HaAnalytics extends LitElement {
});
}
private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
const preference = (checkbox as any).preference;
private _handleRowClick(ev: Event) {
const target = ev.currentTarget as HaSwitch;
const preference = (target as any).preference;
const preferences = this.analytics ? { ...this.analytics.preferences } : {};
if (preferences[preference] === checkbox.checked) {
if (preferences[preference] === target.checked) {
return;
}
preferences[preference] = checkbox.checked;
preferences[preference] = target.checked;
if (
ADDITIONAL_PREFERENCES.some((entry) => entry.key === preference) &&
checkbox.checked
target.checked
) {
preferences.base = true;
} else if (preference === "base" && !checkbox.checked) {
} else if (preference === "base" && !target.checked) {
preferences.usage = false;
preferences.statistics = false;
}

View File

@@ -28,8 +28,8 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-icon-button";
import "./ha-svg-icon";
@@ -49,6 +49,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public value?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: Boolean, attribute: "no-add" })
@@ -84,6 +86,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@state() private _areas?: AreaRegistryEntry[];
@state() private _devices?: DeviceRegistryEntry[];
@@ -310,11 +314,13 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
return html`
<ha-combo-box
.hass=${this.hass}
.helper=${this.helper}
item-value-path="area_id"
item-id-path="area_id"
item-label-path="name"
.value=${this.value}
.disabled=${this.disabled}
.required=${this.required}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area")
: this.label}

View File

@@ -0,0 +1,166 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { EntityRegistryEntry } from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-area-picker";
@customElement("ha-areas-picker")
export class HaAreasPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string[];
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: Boolean, attribute: "no-add" })
public noAdd?: boolean;
/**
* Show only areas with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no areas with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only areas with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property({ attribute: "picked-area-label" })
public pickedAreaLabel?: string;
@property({ attribute: "pick-area-label" })
public pickAreaLabel?: string;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const currentAreas = this._currentAreas;
return html`
${currentAreas.map(
(area) => html`
<div>
<ha-area-picker
.curValue=${area}
.noAdd=${this.noAdd}
.hass=${this.hass}
.value=${area}
.label=${this.pickedAreaLabel}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.disabled=${this.disabled}
@value-changed=${this._areaChanged}
></ha-area-picker>
</div>
`
)}
<div>
<ha-area-picker
.noAdd=${this.noAdd}
.hass=${this.hass}
.label=${this.pickAreaLabel}
.helper=${this.helper}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.required=${this.required && !currentAreas.length}
@value-changed=${this._addArea}
></ha-area-picker>
</div>
`;
}
private get _currentAreas(): string[] {
return this.value || [];
}
private async _updateAreas(areas) {
this.value = areas;
fireEvent(this, "value-changed", {
value: areas,
});
}
private _areaChanged(ev: CustomEvent) {
ev.stopPropagation();
const curValue = (ev.currentTarget as any).curValue;
const newValue = ev.detail.value;
if (newValue === curValue) {
return;
}
const currentAreas = this._currentAreas;
if (!newValue || currentAreas.includes(newValue)) {
this._updateAreas(currentAreas.filter((ent) => ent !== curValue));
return;
}
this._updateAreas(
currentAreas.map((ent) => (ent === curValue ? newValue : ent))
);
}
private _addArea(ev: CustomEvent) {
ev.stopPropagation();
const toAdd = ev.detail.value;
if (!toAdd) {
return;
}
(ev.currentTarget as any).value = "";
const currentAreas = this._currentAreas;
if (currentAreas.includes(toAdd)) {
return;
}
this._updateAreas([...currentAreas, toAdd]);
}
static override styles = css`
div {
margin-top: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-areas-picker": HaAreasPicker;
}
}

View File

@@ -1,12 +1,14 @@
import { LitElement, html, TemplateResult, css } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-select";
import "@material/mwc-list/mwc-list-item";
import "./ha-textfield";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-select";
import "./ha-textfield";
import "./ha-input-helper-text";
export interface TimeChangedEvent {
days?: number;
hours: number;
minutes: number;
seconds: number;
@@ -21,6 +23,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property() label?: string;
/**
* Helper for the input
*/
@property() helper?: string;
/**
* auto validate time inputs
*/
@@ -41,6 +48,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Boolean }) disabled = false;
/**
* day
*/
@property({ type: Number }) days = 0;
/**
* hour
*/
@@ -61,6 +73,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Number }) milliseconds = 0;
/**
* Label for the day input
*/
@property() dayLabel = "";
/**
* Label for the hour input
*/
@@ -91,6 +108,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Boolean }) enableMillisecond = false;
/**
* show the day field
*/
@property({ type: Boolean }) enableDay = false;
/**
* limit hours input
*/
@@ -108,8 +130,33 @@ export class HaBaseTimeInput extends LitElement {
protected render(): TemplateResult {
return html`
${this.label ? html`<label>${this.label}</label>` : ""}
${this.label
? html`<label>${this.label}${this.required ? " *" : ""}</label>`
: ""}
<div class="time-input-wrap">
${this.enableDay
? html`
<ha-textfield
id="day"
type="number"
inputmode="numeric"
.value=${this.days}
.label=${this.dayLabel}
name="days"
@input=${this._valueChanged}
@focus=${this._onFocus}
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
min="0"
.disabled=${this.disabled}
suffix=":"
class="hasSuffix"
>
</ha-textfield>
`
: ""}
<ha-textfield
id="hour"
type="number"
@@ -207,6 +254,9 @@ export class HaBaseTimeInput extends LitElement {
<mwc-list-item value="PM">PM</mwc-list-item>
</ha-select>`}
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}
@@ -260,6 +310,7 @@ export class HaBaseTimeInput extends LitElement {
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
overflow: hidden;
position: relative;
direction: ltr;
}
ha-textfield {
width: 40px;

View File

@@ -117,6 +117,19 @@ export class HaButtonToggleGroup extends LitElement {
--mdc-shape-small: 4px;
border-right-width: 1px;
}
:host([dir="rtl"]) ha-icon-button:first-child,
:host([dir="rtl"]) mwc-button:first-child {
border-radius: 0 4px 4px 0;
border-right-width: 1px;
--mdc-shape-small: 0 4px 4px 0;
--mdc-button-outline-width: 1px;
}
:host([dir="rtl"]) ha-icon-button:last-child,
:host([dir="rtl"]) mwc-button:last-child {
--mdc-shape-small: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
}
`;
}
}

View File

@@ -25,13 +25,7 @@ export class HaChipSet extends LitElement {
${unsafeCSS(chipStyles)}
slot::slotted(ha-chip) {
margin: 4px;
}
slot::slotted(ha-chip:first-of-type) {
margin-left: -4px;
}
slot::slotted(ha-chip:last-of-type) {
margin-right: -4px;
margin: 4px 4px 4px 0;
}
`;
}

View File

@@ -14,6 +14,8 @@ import { customElement, property } from "lit/decorators";
export class HaChip extends LitElement {
@property({ type: Boolean }) public hasIcon = false;
@property({ type: Boolean }) public hasTrailingIcon = false;
@property({ type: Boolean }) public noText = false;
protected render(): TemplateResult {
@@ -30,6 +32,11 @@ export class HaChip extends LitElement {
<span class="mdc-chip__text"><slot></slot></span>
</span>
</span>
${this.hasTrailingIcon
? html`<div class="mdc-chip__icon mdc-chip__icon--trailing">
<slot name="trailing-icon"></slot>
</div>`
: null}
</div>
`;
}
@@ -53,14 +60,20 @@ export class HaChip extends LitElement {
color: var(--ha-chip-text-color, var(--primary-text-color));
}
.mdc-chip__icon--leading {
--mdc-icon-size: 20px;
.mdc-chip__icon--leading,
.mdc-chip__icon--trailing {
--mdc-icon-size: 18px;
line-height: 14px;
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
}
.mdc-chip.no-text
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
margin-right: -4px;
}
span[role="gridcell"] {
line-height: 14px;
}
`;
}
}

View File

@@ -0,0 +1,68 @@
import { ListItemBase } from "@material/mwc-list/mwc-list-item-base";
import { styles } from "@material/mwc-list/mwc-list-item.css";
import { css, CSSResult, html } from "lit";
import { customElement, property, query } from "lit/decorators";
@customElement("ha-clickable-list-item")
export class HaClickableListItem extends ListItemBase {
@property() public href?: string;
@property({ type: Boolean }) public disableHref = false;
// property used only in css
@property({ type: Boolean, reflect: true }) public rtl = false;
@query("a") private _anchor!: HTMLAnchorElement;
public render() {
const r = super.render();
const href = this.href || "";
return html`${this.disableHref
? html`<a aria-role="option">${r}</a>`
: html`<a aria-role="option" href=${href}>${r}</a>`}`;
}
firstUpdated() {
super.firstUpdated();
this.addEventListener("keydown", (ev) => {
if (ev.key === "Enter" || ev.key === " ") {
this._anchor.click();
}
});
}
static get styles(): CSSResult[] {
return [
styles,
css`
:host {
padding-left: 0px;
padding-right: 0px;
}
:host([rtl]) span {
margin-left: var(--mdc-list-item-graphic-margin, 20px) !important;
margin-right: 0px !important;
}
:host([graphic="avatar"]:not([twoLine])),
:host([graphic="icon"]:not([twoLine])) {
height: 48px;
}
a {
width: 100%;
height: 100%;
display: flex;
align-items: center;
padding-left: var(--mdc-list-side-padding, 20px);
padding-right: var(--mdc-list-side-padding, 20px);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-clickable-list-item": HaClickableListItem;
}
}

View File

@@ -64,6 +64,8 @@ export class HaComboBox extends LitElement {
@property() public validationMessage?: string;
@property() public helper?: string;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public invalid?: boolean;
@@ -87,6 +89,8 @@ export class HaComboBox extends LitElement {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean, reflect: true, attribute: "opened" })
private _opened?: boolean;
@@ -108,17 +112,22 @@ export class HaComboBox extends LitElement {
return this._comboBox.selectedItem;
}
public setInputValue(value: string) {
this._comboBox.value = value;
}
protected render(): TemplateResult {
return html`
<vaadin-combo-box-light
.itemValuePath=${this.itemValuePath}
.itemIdPath=${this.itemIdPath}
.itemLabelPath=${this.itemLabelPath}
.value=${this.value || ""}
.items=${this.items}
.value=${this.value || ""}
.filteredItems=${this.filteredItems}
.allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled}
.required=${this.required}
${comboBoxRenderer(this.renderer || this._defaultRowRenderer)}
@opened-changed=${this._openedChanged}
@filter-changed=${this._filterChanged}
@@ -129,6 +138,7 @@ export class HaComboBox extends LitElement {
.label=${this.label}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
.required=${this.required}
.validationMessage=${this.validationMessage}
.errorMessage=${this.errorMessage}
class="input"
@@ -139,6 +149,8 @@ export class HaComboBox extends LitElement {
.suffix=${html`<div style="width: 28px;"></div>`}
.icon=${this.icon}
.invalid=${this.invalid}
.helper=${this.helper}
helperPersistent
>
<slot name="icon" slot="leadingIcon"></slot>
</ha-textfield>
@@ -238,6 +250,18 @@ export class HaComboBox extends LitElement {
top: -7px;
right: 36px;
}
:host-context([style*="direction: rtl;"]) .toggle-button {
left: 12px;
right: auto;
top: -10px;
}
:host-context([style*="direction: rtl;"]) .clear-button {
--mdc-icon-size: 20px;
top: -7px;
left: 36px;
right: auto;
}
`;
}
}

View File

@@ -35,17 +35,24 @@ export class HaDateInput extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property() public label?: string;
@property() public helper?: string;
render() {
return html`<ha-textfield
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
iconTrailing
helperPersistent
@click=${this._openDialog}
.value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)
: ""}
.required=${this.required}
>
<ha-svg-icon slot="trailingIcon" .path=${mdiCalendar}></ha-svg-icon>
</ha-textfield>`;

View File

@@ -98,6 +98,10 @@ export class HaDialog extends DialogBase {
margin-left: 40px;
margin-right: 0px;
}
:host-context([style*="direction: rtl;"]) .dialog-actions {
left: 0px !important;
right: auto !important;
}
`,
];
}

View File

@@ -5,6 +5,7 @@ import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
export interface HaDurationData {
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
@@ -17,10 +18,14 @@ class HaDurationInput extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean }) public enableMillisecond?: boolean;
@property({ type: Boolean }) public enableDay?: boolean;
@property({ type: Boolean }) public disabled = false;
@query("paper-time-input", true) private _input?: HTMLElement;
@@ -35,19 +40,23 @@ class HaDurationInput extends LitElement {
return html`
<ha-base-time-input
.label=${this.label}
.helper=${this.helper}
.required=${this.required}
.autoValidate=${this.required}
.disabled=${this.disabled}
errorMessage="Required"
enableSecond
.enableMillisecond=${this.enableMillisecond}
.enableDay=${this.enableDay}
format="24"
.days=${this._days}
.hours=${this._hours}
.minutes=${this._minutes}
.seconds=${this._seconds}
.milliseconds=${this._milliseconds}
@value-changed=${this._durationChanged}
noHoursLimit
dayLabel="dd"
hourLabel="hh"
minLabel="mm"
secLabel="ss"
@@ -56,6 +65,10 @@ class HaDurationInput extends LitElement {
`;
}
private get _days() {
return this.data?.days ? Number(this.data.days) : 0;
}
private get _hours() {
return this.data?.hours ? Number(this.data.hours) : 0;
}
@@ -94,6 +107,11 @@ class HaDurationInput extends LitElement {
value.minutes %= 60;
}
if (this.enableDay && value.hours > 24) {
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
value.hours %= 24;
}
fireEvent(this, "value-changed", {
value,
});

View File

@@ -1,5 +1,6 @@
import { Fab } from "@material/mwc-fab";
import { customElement } from "lit/decorators";
import { css } from "lit";
@customElement("ha-fab")
export class HaFab extends Fab {
@@ -7,6 +8,17 @@ export class HaFab extends Fab {
super.firstUpdated(changedProperties);
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
}
static override styles = Fab.styles.concat([
css`
:host-context([style*="direction: rtl;"])
.mdc-fab--extended
.mdc-fab__icon {
margin-left: 12px !important;
margin-right: calc(12px - 20px) !important;
}
`,
]);
}
declare global {

View File

@@ -176,10 +176,24 @@ export class HaFileUpload extends LitElement {
.mdc-text-field__icon--leading {
margin-bottom: 12px;
}
:host-context([style*="direction: rtl;"])
.mdc-text-field__icon--leading {
margin-right: 0px;
}
.mdc-text-field--filled .mdc-floating-label--float-above {
transform: scale(0.75);
top: 8px;
}
:host-context([style*="direction: rtl;"]) .mdc-floating-label {
left: initial;
right: 16px;
}
:host-context([style*="direction: rtl;"])
.mdc-text-field--filled
.mdc-floating-label {
left: initial;
right: 48px;
}
.dragged:before {
position: var(--layout-fit_-_position);
top: var(--layout-fit_-_top);

View File

@@ -34,12 +34,25 @@ export const computeInitialHaFormData = (
};
} else if ("selector" in field) {
const selector: Selector = field.selector;
if ("boolean" in selector) {
if ("device" in selector) {
data[field.name] = selector.device.multiple ? [] : "";
} else if ("entity" in selector) {
data[field.name] = selector.entity.multiple ? [] : "";
} else if ("area" in selector) {
data[field.name] = selector.area.multiple ? [] : "";
} else if ("boolean" in selector) {
data[field.name] = false;
} else if ("text" in selector) {
} else if (
"text" in selector ||
"addon" in selector ||
"attribute" in selector ||
"icon" in selector ||
"theme" in selector
) {
data[field.name] = "";
} else if ("number" in selector) {
data[field.name] = "min" in selector.number ? selector.number.min : 0;
data[field.name] = selector.number.min ?? 0;
} else if ("select" in selector) {
if (selector.select.options.length) {
data[field.name] = selector.select.options[0][0];
@@ -50,6 +63,23 @@ export const computeInitialHaFormData = (
minutes: 0,
seconds: 0,
};
} else if ("time" in selector) {
data[field.name] = "00:00:00";
} else if ("date" in selector || "datetime" in selector) {
const now = new Date().toISOString().slice(0, 10);
data[field.name] = `${now} 00:00:00`;
} else if ("color_rgb" in selector) {
data[field.name] = [0, 0, 0];
} else if ("color_temp" in selector) {
data[field.name] = selector.color_temp.min_mireds ?? 153;
} else if (
"action" in selector ||
"media" in selector ||
"target" in selector
) {
data[field.name] = {};
} else {
throw new Error("Selector not supported in initial form data");
}
}
});

View File

@@ -28,6 +28,8 @@ export class HaFormString extends LitElement implements HaFormElement {
@property() public label!: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@state() private _unmaskedPassword = false;
@@ -53,6 +55,8 @@ export class HaFormString extends LitElement implements HaFormElement {
: "password"}
.label=${this.label}
.value=${this.data || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
.required=${this.schema.required}
.autoValidate=${this.schema.required}

View File

@@ -77,7 +77,7 @@ export class HaForm extends LitElement implements HaFormElement {
protected render(): TemplateResult {
return html`
<div class="root">
<div class="root" part="root">
${this.error && this.error.base
? html`
<ha-alert alert-type="error">
@@ -173,7 +173,6 @@ export class HaForm extends LitElement implements HaFormElement {
}
static get styles(): CSSResultGroup {
// .root has overflow: auto to avoid margin collapse
return css`
.root {
margin-bottom: -24px;

View File

@@ -31,6 +31,8 @@ export class HaIconPicker extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property() public fallbackPath?: string;
@@ -39,6 +41,8 @@ export class HaIconPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public invalid = false;
@state() private _opened = false;
@@ -55,7 +59,9 @@ export class HaIconPicker extends LitElement {
allow-custom-value
.filteredItems=${iconItems}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.placeholder=${this.placeholder}
.errorMessage=${this.errorMessage}
.invalid=${this.invalid}

View File

@@ -0,0 +1,25 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-input-helper-text")
class InputHelperText extends LitElement {
protected render(): TemplateResult {
return html`<slot></slot>`;
}
static styles = css`
:host {
display: block;
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
font-size: 0.75rem;
padding-left: 16px;
padding-right: 16px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-input-helper-text": InputHelperText;
}
}

View File

@@ -33,7 +33,7 @@ class HaLabeledSlider extends PolymerElement {
}
</style>
<div class="title">[[caption]]</div>
<div class="title">[[_getTitle()]]</div>
<div class="extra-container"><slot name="extra"></slot></div>
<div class="slider-container">
<ha-icon icon="[[icon]]" hidden$="[[!icon]]"></ha-icon>
@@ -46,17 +46,26 @@ class HaLabeledSlider extends PolymerElement {
value="{{value}}"
></ha-slider>
</div>
<template is="dom-if" if="[[helper]]">
<ha-input-helper-text>[[helper]]</ha-input-helper-text>
</template>
`;
}
_getTitle() {
return `${this.caption}${this.caption && this.required ? " *" : ""}`;
}
static get properties() {
return {
caption: String,
disabled: Boolean,
required: Boolean,
min: Number,
max: Number,
pin: Boolean,
step: Number,
helper: String,
extra: {
type: Boolean,

View File

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

View File

@@ -3,7 +3,6 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { subscribeNotifications } from "../data/persistent_notification";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
@@ -43,18 +42,15 @@ class HaMenuButton extends LitElement {
protected render(): TemplateResult {
const hasNotifications =
(this.narrow || this.hass.dockedSidebar === "always_hidden") &&
(this._hasNotifications ||
Object.keys(this.hass.states).some(
(entityId) => computeDomain(entityId) === "configurator"
));
this._hasNotifications &&
(this.narrow || this.hass.dockedSidebar === "always_hidden");
return html`
<ha-icon-button
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.path=${mdiMenu}
@click=${this._toggleMenu}
></ha-icon-button>
${hasNotifications ? html` <div class="dot"></div> ` : ""}
${hasNotifications ? html`<div class="dot"></div>` : ""}
`;
}

View File

@@ -0,0 +1,79 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { roundWithOneDecimal } from "../util/calculate";
import "./ha-bar";
import "./ha-settings-row";
@customElement("ha-metric")
class HaMetric extends LitElement {
@property({ type: Number }) public value!: number;
@property({ type: String }) public heading!: string;
@property({ type: String }) public tooltip?: string;
protected render(): TemplateResult {
const roundedValue = roundWithOneDecimal(this.value);
return html`
<ha-settings-row>
<span slot="heading"> ${this.heading} </span>
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value"> ${roundedValue} % </span>
<ha-bar
class=${classMap({
"target-warning": roundedValue > 50,
"target-critical": roundedValue > 85,
})}
.value=${this.value}
></ha-bar>
</div>
</ha-settings-row>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row > div[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
display: flex;
justify-content: space-between;
}
ha-bar {
--ha-bar-primary-color: var(
--metric-bar-ok-color,
var(--success-color)
);
}
.target-warning {
--ha-bar-primary-color: var(
--metric-bar-warning-color,
var(--warning-color)
);
}
.target-critical {
--ha-bar-primary-color: var(
--metric-bar-critical-color,
var(--error-color)
);
}
.value {
width: 48px;
padding-right: 4px;
flex-shrink: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-metric": HaMetric;
}
}

View File

@@ -0,0 +1,93 @@
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../types";
import "./ha-clickable-list-item";
import "./ha-icon-next";
import "./ha-svg-icon";
@customElement("ha-navigation-list")
class HaNavigationList extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public pages!: PageNavigation[];
@property({ type: Boolean }) public hasSecondary = false;
public render(): TemplateResult {
return html`
<mwc-list>
${this.pages.map(
(page) => html`
<ha-clickable-list-item
graphic="avatar"
.twoline=${this.hasSecondary}
.hasMeta=${!this.narrow}
@click=${this._entryClicked}
href=${page.path}
>
<div
slot="graphic"
class=${page.iconColor ? "icon-background" : ""}
.style="background-color: ${page.iconColor || "undefined"}"
>
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
</div>
<span>${page.name}</span>
${this.hasSecondary
? html`<span slot="secondary">${page.description}</span>`
: ""}
${!this.narrow
? html`<ha-icon-next slot="meta"></ha-icon-next>`
: ""}
</ha-clickable-list-item>
`
)}
</mwc-list>
`;
}
private _entryClicked(ev) {
ev.currentTarget.blur();
}
static styles: CSSResultGroup = css`
a {
text-decoration: none;
color: var(--primary-text-color);
position: relative;
display: block;
outline: 0;
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
height: 24px;
width: 24px;
}
ha-svg-icon {
padding: 8px;
}
.icon-background {
border-radius: 50%;
}
.icon-background ha-svg-icon {
color: #fff;
}
ha-clickable-list-item {
cursor: pointer;
font-size: var(--navigation-list-item-title-font-size);
padding: var(--navigation-list-item-padding) 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-navigation-list": HaNavigationList;
}
}

View File

@@ -47,6 +47,10 @@ export class HaSelect extends SelectBase {
.mdc-select__anchor {
width: var(--ha-select-min-width, 200px);
}
:host-context([style*="direction: rtl;"]) .mdc-floating-label {
right: 16px !important;
left: initial !important;
}
`,
];
}

View File

@@ -14,11 +14,20 @@ export class HaAddonSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`<ha-addon-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
></ha-addon-picker>`;
}

View File

@@ -6,6 +6,7 @@ import { EntityRegistryEntry } from "../../data/entity_registry";
import { AreaSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-area-picker";
import "../ha-areas-picker";
@customElement("ha-selector-area")
export class HaAreaSelector extends LitElement {
@@ -17,10 +18,14 @@ export class HaAreaSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@state() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
@@ -38,21 +43,47 @@ export class HaAreaSelector extends LitElement {
}
protected render() {
return html`<ha-area-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-area-picker>`;
if (!this.selector.area.multiple) {
return html`
<ha-area-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
></ha-area-picker>
`;
}
return html`
<ha-areas-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.pickAreaLabel=${this.label}
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
></ha-areas-picker>
`;
}
private _filterEntities = (entity: EntityRegistryEntry): boolean => {

View File

@@ -1,10 +1,10 @@
import "../entity/ha-entity-attribute-picker";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { AttributeSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import "../entity/ha-entity-attribute-picker";
@customElement("ha-selector-attribute")
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@@ -16,8 +16,12 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@property() public context?: {
filter_entity?: string;
};
@@ -30,7 +34,9 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
this.context?.filter_entity}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-value
></ha-entity-attribute-picker>
`;

View File

@@ -4,6 +4,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "../ha-formfield";
import "../ha-switch";
import "../ha-input-helper-text";
@customElement("ha-selector-boolean")
export class HaBooleanSelector extends LitElement {
@@ -13,16 +14,23 @@ export class HaBooleanSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>
</ha-formfield>`;
return html`
<ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>
</ha-formfield>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}
private _handleChange(ev) {
@@ -35,12 +43,10 @@ export class HaBooleanSelector extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
height: 56px;
display: flex;
}
ha-formfield {
width: 100%;
display: flex;
height: 56px;
align-items: center;
--mdc-typography-body2-font-size: 1em;
}
`;

View File

@@ -1,9 +1,9 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { ColorRGBSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
import { hex2rgb, rgb2hex } from "../../common/color/convert-color";
import { fireEvent } from "../../common/dom/fire_event";
import type { ColorRGBSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-textfield";
@customElement("ha-selector-color_rgb")
@@ -16,14 +16,22 @@ export class HaColorRGBSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-textfield
type="color"
helperPersistent
.value=${this.value ? rgb2hex(this.value as any) : ""}
.label=${this.label || ""}
.required=${this.required}
.helper=${this.helper}
.disalbled=${this.disabled}
@change=${this._valueChanged}
></ha-textfield>
`;

View File

@@ -1,8 +1,8 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { ColorTempSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
import type { ColorTempSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-labeled-slider";
@customElement("ha-selector-color_temp")
@@ -15,17 +15,24 @@ export class HaColorTempSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-labeled-slider
pin
icon="hass:thermometer"
.caption=${this.label}
.caption=${this.label || ""}
.min=${this.selector.color_temp.min_mireds ?? 153}
.max=${this.selector.color_temp.max_mireds ?? 500}
.value=${this.value}
.disabled=${this.disabled}
.helper=${this.helper}
.required=${this.required}
@change=${this._valueChanged}
></ha-labeled-slider>
`;

View File

@@ -1,7 +1,7 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { DateSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-date-input";
@customElement("ha-selector-date")
@@ -14,8 +14,12 @@ export class HaDateSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-date-input
@@ -23,6 +27,8 @@ export class HaDateSelector extends LitElement {
.locale=${this.hass.locale}
.disabled=${this.disabled}
.value=${this.value}
.required=${this.required}
.helper=${this.helper}
>
</ha-date-input>
`;

View File

@@ -1,12 +1,13 @@
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { DateTimeSelector } from "../../data/selector";
import type { HaDateInput } from "../ha-date-input";
import type { HaTimeInput } from "../ha-time-input";
import { fireEvent } from "../../common/dom/fire_event";
import type { DateTimeSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-date-input";
import type { HaDateInput } from "../ha-date-input";
import "../ha-time-input";
import "../ha-input-helper-text";
import type { HaTimeInput } from "../ha-time-input";
@customElement("ha-selector-datetime")
export class HaDateTimeSelector extends LitElement {
@@ -18,8 +19,12 @@ export class HaDateTimeSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@query("ha-date-input") private _dateInput!: HaDateInput;
@query("ha-time-input") private _timeInput!: HaTimeInput;
@@ -28,21 +33,28 @@ export class HaDateTimeSelector extends LitElement {
const values = this.value?.split(" ");
return html`
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.value=${values?.[0]}
@value-changed=${this._valueChanged}
>
</ha-date-input>
<ha-time-input
enable-second
.value=${values?.[1] || "0:00:00"}
.locale=${this.hass.locale}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
></ha-time-input>
<div class="input">
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
.value=${values?.[0]}
@value-changed=${this._valueChanged}
>
</ha-date-input>
<ha-time-input
enable-second
.value=${values?.[1] || "0:00:00"}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
@value-changed=${this._valueChanged}
></ha-time-input>
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}
@@ -54,7 +66,7 @@ export class HaDateTimeSelector extends LitElement {
}
static styles = css`
:host {
.input {
display: flex;
align-items: center;
flex-direction: row;

View File

@@ -17,10 +17,14 @@ export class HaDeviceSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@state() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
@@ -36,10 +40,32 @@ export class HaDeviceSelector extends LitElement {
protected render() {
if (!this.selector.device.multiple) {
return html`<ha-device-picker
return html`
<ha-device-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
></ha-device-picker>
`;
}
return html`
${this.label ? html`<label>${this.label}</label>` : ""}
<ha-devices-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
@@ -48,21 +74,7 @@ export class HaDeviceSelector extends LitElement {
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
allow-custom-entity
></ha-device-picker> `;
}
return html`
${this.label ? html`<label>${this.label}</label>` : ""}
<ha-devices-picker
.hass=${this.hass}
.value=${this.value}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.required=${this.required}
></ha-devices-picker>
`;
}

View File

@@ -14,6 +14,8 @@ export class HaTimeDuration extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -22,9 +24,11 @@ export class HaTimeDuration extends LitElement {
return html`
<ha-duration-input
.label=${this.label}
.helper=${this.helper}
.data=${this.value}
.disabled=${this.disabled}
.required=${this.required}
.enableDay=${this.selector.duration.enable_day}
></ha-duration-input>
`;
}

View File

@@ -1,38 +1,46 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { subscribeEntityRegistry } from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import { EntitySelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entities-picker";
import "../entity/ha-entity-picker";
@customElement("ha-selector-entity")
export class HaEntitySelector extends SubscribeMixin(LitElement) {
export class HaEntitySelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: EntitySelector;
@state() private _entityPlaformLookup?: Record<string, string>;
@state() private _entitySources?: EntitySources;
@property() public value?: any;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
if (!this.selector.entity.multiple) {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.includeEntities=${this.selector.entity.includeEntities}
.excludeEntities=${this.selector.entity.excludeEntities}
.helper=${this.helper}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
></ha-entity-picker>`;
}
@@ -42,56 +50,57 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
<ha-entities-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.entityFilter=${this._filterEntities}
.includeEntities=${this.selector.entity.includeEntities}
.excludeEntities=${this.selector.entity.excludeEntities}
.disabled=${this.disabled}
.required=${this.required}
></ha-entities-picker>
`;
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
const entityLookup = {};
for (const confEnt of entities) {
if (!confEnt.platform) {
continue;
}
entityLookup[confEnt.entity_id] = confEnt.platform;
}
this._entityPlaformLookup = entityLookup;
}),
];
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (
changedProps.has("selector") &&
this.selector.entity.integration &&
!this._entitySources
) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
this._entitySources = sources;
});
}
}
private _filterEntities = (entity: HassEntity): boolean => {
if (this.selector.entity?.domain) {
const filterDomain = this.selector.entity.domain;
const filterDomainIsArray = Array.isArray(filterDomain);
const {
domain: filterDomain,
device_class: filterDeviceClass,
integration: filterIntegration,
} = this.selector.entity;
if (filterDomain) {
const entityDomain = computeStateDomain(entity);
if (
(filterDomainIsArray && !filterDomain.includes(entityDomain)) ||
(!filterDomainIsArray && entityDomain !== filterDomain)
Array.isArray(filterDomain)
? !filterDomain.includes(entityDomain)
: entityDomain !== filterDomain
) {
return false;
}
}
if (this.selector.entity?.device_class) {
if (
!entity.attributes.device_class ||
entity.attributes.device_class !== this.selector.entity.device_class
) {
return false;
}
if (
filterDeviceClass &&
entity.attributes.device_class !== filterDeviceClass
) {
return false;
}
if (this.selector.entity?.integration) {
if (
!this._entityPlaformLookup ||
this._entityPlaformLookup[entity.entity_id] !==
this.selector.entity.integration
) {
return false;
}
if (
filterIntegration &&
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
) {
return false;
}
return true;
};

View File

@@ -1,9 +1,9 @@
import "../ha-icon-picker";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { HomeAssistant } from "../../types";
import { IconSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
import { IconSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-icon-picker";
@customElement("ha-selector-icon")
export class HaIconSelector extends LitElement {
@@ -15,13 +15,20 @@ export class HaIconSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-icon-picker
.label=${this.label}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
.fallbackPath=${this.selector.icon.fallbackPath}
.placeholder=${this.selector.icon.placeholder}
@value-changed=${this._valueChanged}

View File

@@ -1,4 +1,4 @@
import { html, LitElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
@@ -6,7 +6,6 @@ import type {
LocationSelector,
LocationSelectorValue,
} from "../../data/selector";
import "../../panels/lovelace/components/hui-theme-select-editor";
import type { HomeAssistant } from "../../types";
import type { MarkerLocation } from "../map/ha-locations-editor";
import "../map/ha-locations-editor";
@@ -21,6 +20,8 @@ export class HaLocationSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
@@ -28,6 +29,7 @@ export class HaLocationSelector extends LitElement {
<ha-locations-editor
class="flex"
.hass=${this.hass}
.helper=${this.helper}
.locations=${this._location(this.selector, this.value)}
@location-updated=${this._locationChanged}
@radius-updated=${this._radiusChanged}
@@ -52,7 +54,10 @@ export class HaLocationSelector extends LitElement {
longitude: value?.longitude || this.hass.config.longitude,
radius: selector.location.radius ? value?.radius || 1000 : undefined,
radius_color: zoneRadiusColor,
icon: selector.location.icon,
icon:
selector.location.icon || selector.location.radius
? "mdi:map-marker-radius"
: "mdi:map-marker",
location_editable: true,
radius_editable: true,
},
@@ -71,6 +76,13 @@ export class HaLocationSelector extends LitElement {
const radius = ev.detail.radius;
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
}
static styles = css`
:host {
display: block;
height: 400px;
}
`;
}
declare global {

View File

@@ -33,8 +33,12 @@ export class HaMediaSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean, reflect: true }) public required = true;
@state() private _thumbnailUrl?: string | null;
willUpdate(changedProps: PropertyValues<this>) {
@@ -84,6 +88,8 @@ export class HaMediaSelector extends LitElement {
.label=${this.label ||
this.hass.localize("ui.components.selectors.media.pick_media_player")}
.disabled=${this.disabled}
.helper=${this.helper}
.required=${this.required}
include-domains='["media_player"]'
allow-custom-entity
@value-changed=${this._entityChanged}

View File

@@ -6,6 +6,7 @@ import { NumberSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-slider";
import "../ha-textfield";
import "../ha-input-helper-text";
@customElement("ha-selector-number")
export class HaNumberSelector extends LitElement {
@@ -19,13 +20,20 @@ export class HaNumberSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public required = true;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`${this.selector.number.mode !== "box"
? html`${this.label}<ha-slider
const isBox = this.selector.number.mode === "box";
return html`
${this.label ? html`${this.label}${this.required ? " *" : ""}` : ""}
<div class="input">
${!isBox
? html`<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
@@ -37,26 +45,33 @@ export class HaNumberSelector extends LitElement {
@change=${this._handleSliderChange}
>
</ha-slider>`
: ""}
<ha-textfield
inputMode="numeric"
pattern="[0-9]+([\\.][0-9]+)?"
.label=${this.selector.number.mode !== "box" ? undefined : this.label}
.placeholder=${this.placeholder}
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value ?? ""}
.step=${this.selector.number.step ?? 1}
helperPersistent
.helper=${isBox ? this.helper : undefined}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${this.selector.number.unit_of_measurement}
type="number"
autoValidate
?no-spinner=${this.selector.number.mode !== "box"}
@input=${this._handleInputChange}
>
</ha-textfield>
</div>
${!isBox && this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
<ha-textfield
inputMode="numeric"
pattern="[0-9]+([\\.][0-9]+)?"
.label=${this.selector.number.mode !== "box" ? undefined : this.label}
.placeholder=${this.placeholder}
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value ?? ""}
.step=${this.selector.number.step ?? 1}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${this.selector.number.unit_of_measurement}
type="number"
autoValidate
?no-spinner=${this.selector.number.mode !== "box"}
@input=${this._handleInputChange}
>
</ha-textfield>`;
`;
}
private get _value() {
@@ -88,10 +103,11 @@ export class HaNumberSelector extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
.input {
display: flex;
justify-content: space-between;
align-items: center;
direction: ltr;
}
ha-slider {
flex: 1;

View File

@@ -3,6 +3,7 @@ import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "../ha-yaml-editor";
import "../ha-input-helper-text";
@customElement("ha-selector-object")
export class HaObjectSelector extends LitElement {
@@ -12,18 +13,27 @@ export class HaObjectSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`<ha-yaml-editor
.hass=${this.hass}
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
@value-changed=${this._handleChange}
></ha-yaml-editor>`;
.hass=${this.hass}
.readonly=${this.disabled}
.label=${this.label}
.required=${this.required}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
@value-changed=${this._handleChange}
></ha-yaml-editor>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""} `;
}
private _handleChange(ev) {

View File

@@ -1,13 +1,19 @@
import "@material/mwc-formfield/mwc-formfield";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { SelectOption, SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-select";
import "../ha-checkbox";
import "../ha-chip";
import "../ha-chip-set";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-formfield";
import "../ha-radio";
import "../ha-select";
@customElement("ha-selector-select")
export class HaSelectSelector extends LitElement {
@@ -15,7 +21,7 @@ export class HaSelectSelector extends LitElement {
@property({ attribute: false }) public selector!: SelectSelector;
@property() public value?: string;
@property() public value?: string | string[];
@property() public label?: string;
@@ -25,27 +31,117 @@ export class HaSelectSelector extends LitElement {
@property({ type: Boolean }) public required = true;
@query("ha-combo-box", true) private comboBox!: HaComboBox;
private _filter = "";
protected render() {
if (this.required && this.selector.select.options!.length < 6) {
const options = this.selector.select.options.map((option) =>
typeof option === "object" ? option : { value: option, label: option }
);
if (!this.selector.select.custom_value && this._mode === "list") {
if (!this.selector.select.multiple || this.required) {
return html`
<div>
${this.label}
${options.map(
(item: SelectOption) => html`
<mwc-formfield .label=${item.label}>
<ha-radio
.checked=${item.value === this.value}
.value=${item.value}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-radio>
</mwc-formfield>
`
)}
</div>
${this._renderHelper()}
`;
}
return html`
<div>
${this.label}
${this.selector.select.options.map((item: string | SelectOption) => {
const value = typeof item === "object" ? item.value : item;
const label = typeof item === "object" ? item.label : item;
return html`
<mwc-formfield .label=${label}>
<ha-radio
.checked=${value === this.value}
.value=${value}
${this.label}${options.map(
(item: SelectOption) => html`
<ha-formfield .label=${item.label}>
<ha-checkbox
.checked=${this.value?.includes(item.value)}
.value=${item.value}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-radio>
</mwc-formfield>
`;
})}
@change=${this._checkboxChanged}
></ha-checkbox>
</ha-formfield>
`
)}
</div>
${this._renderHelper()}
`;
}
if (this.selector.select.multiple) {
const value =
!this.value || this.value === "" ? [] : (this.value as string[]);
return html`
<ha-chip-set>
${value?.map(
(item, idx) =>
html`
<ha-chip hasTrailingIcon>
${options.find((option) => option.value === item)?.label ||
item}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiClose}
.idx=${idx}
@click=${this._removeItem}
></ha-svg-icon>
</ha-chip>
`
)}
</ha-chip-set>
<ha-combo-box
item-value-path="value"
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.value=${this._filter}
.items=${options.filter((item) => !this.value?.includes(item.value))}
@filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged}
></ha-combo-box>
`;
}
if (this.selector.select.custom_value) {
if (
this.value !== undefined &&
!options.find((option) => option.value === this.value)
) {
options.unshift({ value: this.value, label: this.value });
}
return html`
<ha-combo-box
item-value-path="value"
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.items=${options}
.value=${this.value}
@filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged}
></ha-combo-box>
`;
}
@@ -60,36 +156,137 @@ export class HaSelectSelector extends LitElement {
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
${this.selector.select.options.map((item: string | SelectOption) => {
const value = typeof item === "object" ? item.value : item;
const label = typeof item === "object" ? item.label : item;
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
})}
${options.map(
(item: SelectOption) => html`
<mwc-list-item .value=${item.value}>${item.label}</mwc-list-item>
`
)}
</ha-select>
`;
}
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: "";
}
private get _mode(): "list" | "dropdown" {
return (
this.selector.select.mode ||
(this.selector.select.options.length < 6 ? "list" : "dropdown")
);
}
private _valueChanged(ev) {
ev.stopPropagation();
if (this.disabled || !ev.target.value) {
const value = ev.detail?.value || ev.target.value;
if (this.disabled || !value) {
return;
}
fireEvent(this, "value-changed", {
value: ev.target.value,
value: value,
});
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
private _checkboxChanged(ev) {
ev.stopPropagation();
if (this.disabled) {
return;
}
let newValue: string[];
const value: string = ev.target.value;
const checked = ev.target.checked;
if (checked) {
if (!this.value) {
newValue = [value];
} else if (this.value.includes(value)) {
return;
} else {
newValue = [...this.value, value];
}
mwc-formfield {
display: block;
} else {
if (!this.value?.includes(value)) {
return;
}
`;
newValue = (this.value as string[]).filter((v) => v !== value);
}
fireEvent(this, "value-changed", {
value: newValue,
});
}
private async _removeItem(ev) {
const value: string[] = [...(this.value! as string[])];
value.splice(ev.target.idx, 1);
fireEvent(this, "value-changed", {
value,
});
await this.updateComplete;
this._filterChanged();
}
private _comboBoxValueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newValue = ev.detail.value;
if (this.disabled || newValue === "") {
return;
}
if (!this.selector.select.multiple) {
fireEvent(this, "value-changed", {
value: newValue,
});
return;
}
if (newValue !== undefined && this.value?.includes(newValue)) {
return;
}
setTimeout(() => {
this._filterChanged();
this.comboBox.setInputValue("");
}, 0);
const currentValue =
!this.value || this.value === "" ? [] : (this.value as string[]);
fireEvent(this, "value-changed", {
value: [...currentValue, newValue],
});
}
private _filterChanged(ev?: CustomEvent): void {
this._filter = ev?.detail.value || "";
const filteredItems = this.comboBox.items?.filter((item) => {
if (this.selector.select.multiple && this.value?.includes(item.value)) {
return false;
}
const label = item.label || item.value;
return label.toLowerCase().includes(this._filter?.toLowerCase());
});
if (this._filter && this.selector.select.custom_value) {
filteredItems?.unshift({ label: this._filter, value: this._filter });
}
this.comboBox.filteredItems = filteredItems;
}
static styles = css`
ha-select,
mwc-formfield,
ha-formfield {
display: block;
}
`;
}
declare global {

View File

@@ -26,6 +26,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property() public helper?: string;
@state() private _entityPlaformLookup?: Record<string, string>;
@state() private _configEntries?: ConfigEntry[];
@@ -64,6 +66,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
return html`<ha-target-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.entityRegFilter=${this._filterRegEntities}
.entityFilter=${this._filterEntities}

View File

@@ -0,0 +1,56 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "../ha-code-editor";
import "../ha-input-helper-text";
@customElement("ha-selector-template")
export class HaTemplateSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public value?: string;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
${this.label
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
: ""}
<ha-code-editor
mode="jinja2"
.hass=${this.hass}
.value=${this.value}
.readOnly=${this.disabled}
autofocus
autocomplete-entities
@value-changed=${this._handleChange}
dir="ltr"
></ha-code-editor>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}
private _handleChange(ev) {
const value = ev.target.value;
if (this.value === value) {
return;
}
fireEvent(this, "value-changed", { value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-template": HaTemplateSelector;
}
}

View File

@@ -18,6 +18,8 @@ export class HaTextSelector extends LitElement {
@property() public placeholder?: string;
@property() public helper?: string;
@property() public selector!: StringSelector;
@property({ type: Boolean }) public disabled = false;
@@ -32,6 +34,8 @@ export class HaTextSelector extends LitElement {
.label=${this.label}
.placeholder=${this.placeholder}
.value=${this.value || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
@input=${this._handleChange}
autocapitalize="none"
@@ -44,6 +48,8 @@ export class HaTextSelector extends LitElement {
return html`<ha-textfield
.value=${this.value || ""}
.placeholder=${this.placeholder || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
@input=${this._handleChange}

View File

@@ -1,8 +1,8 @@
import "../../panels/lovelace/components/hui-theme-select-editor";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { ThemeSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-theme-picker";
@customElement("ha-selector-theme")
export class HaThemeSelector extends LitElement {
@@ -16,13 +16,17 @@ export class HaThemeSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<hui-theme-select-editor
<ha-theme-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
></hui-theme-select-editor>
.disabled=${this.disabled}
.required=${this.required}
></ha-theme-picker>
`;
}
}

View File

@@ -14,14 +14,20 @@ export class HaTimeSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
protected render() {
return html`
<ha-time-input
.value=${this.value}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
.helper=${this.helper}
.label=${this.label}
enable-second
></ha-time-input>

View File

@@ -18,6 +18,7 @@ import "./ha-selector-number";
import "./ha-selector-object";
import "./ha-selector-select";
import "./ha-selector-target";
import "./ha-selector-template";
import "./ha-selector-text";
import "./ha-selector-time";
import "./ha-selector-icon";

View File

@@ -471,6 +471,7 @@ export class HaServiceControl extends LitElement {
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
--settings-row-content-width: 100%;
border-top: var(
--service-control-items-border-top,
1px solid var(--divider-color)
@@ -489,9 +490,6 @@ export class HaServiceControl extends LitElement {
margin: var(--service-control-padding, 0 16px);
padding: 16px 0;
}
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
}
.checkbox-spacer {
width: 32px;
}

View File

@@ -21,7 +21,7 @@ export class HaSettingsRow extends LitElement {
<div secondary><slot name="description"></slot></div>
</paper-item-body>
</div>
<slot></slot>
<div class="content"><slot></slot></div>
`;
}
@@ -43,6 +43,18 @@ export class HaSettingsRow extends LitElement {
);
flex: 1;
}
.content {
display: contents;
}
:host(:not([narrow])) .content {
display: flex;
justify-content: flex-end;
flex: 1;
padding: 16px 0;
}
.content ::slotted(*) {
width: var(--settings-row-content-width);
}
:host([narrow]) {
align-items: normal;
flex-direction: column;

View File

@@ -36,14 +36,15 @@ import memoizeOne from "memoize-one";
import { LocalStorage } from "../common/decorators/local-storage";
import { fireEvent } from "../common/dom/fire_event";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import { computeDomain } from "../common/entity/compute_domain";
import { stringCompare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl";
import { throttle } from "../common/util/throttle";
import { ActionHandlerDetail } from "../data/lovelace";
import {
PersistentNotification,
subscribeNotifications,
} from "../data/persistent_notification";
import { updateCanInstall, UpdateEntity } from "../data/update";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -68,7 +69,6 @@ const SORT_VALUE_URL_PATHS = {
const PANEL_ICONS = {
calendar: mdiCalendar,
config: mdiCog,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
history: mdiChartBox,
@@ -190,6 +190,8 @@ class HaSidebar extends LitElement {
@state() private _notifications?: PersistentNotification[];
@state() private _updatesCount = 0;
@state() private _renderEmptySortable = false;
private _mouseLeaveTimeout?: number;
@@ -235,6 +237,7 @@ class HaSidebar extends LitElement {
changedProps.has("narrow") ||
changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_updatesCount") ||
changedProps.has("_notifications") ||
changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") ||
@@ -290,6 +293,8 @@ class HaSidebar extends LitElement {
toggleAttribute(this, "rtl", computeRTL(this.hass));
}
this._calculateCounts();
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
@@ -302,6 +307,21 @@ class HaSidebar extends LitElement {
}
}
private _calculateCounts = throttle(() => {
let updateCount = 0;
for (const entityId of Object.keys(this.hass.states)) {
if (
entityId.startsWith("update.") &&
updateCanInstall(this.hass.states[entityId] as UpdateEntity)
) {
updateCount++;
}
}
this._updatesCount = updateCount;
}, 5000);
private _renderHeader() {
return html`<div
class="menu"
@@ -387,35 +407,37 @@ class HaSidebar extends LitElement {
icon?: string | null,
iconPath?: string | null
) {
return html`
<a
role="option"
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span>
</paper-icon-item>
${this.editMode
? html`<ha-icon-button
.label=${this.hass.localize("ui.sidebar.hide_panel")}
.path=${mdiClose}
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
></ha-icon-button>`
: ""}
</a>
`;
return urlPath === "config"
? this._renderConfiguration(title)
: html`
<a
role="option"
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span>
</paper-icon-item>
${this.editMode
? html`<ha-icon-button
.label=${this.hass.localize("ui.sidebar.hide_panel")}
.path=${mdiClose}
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
></ha-icon-button>`
: ""}
</a>
`;
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
@@ -477,15 +499,39 @@ class HaSidebar extends LitElement {
return html`<div class="spacer" disabled></div>`;
}
private _renderConfiguration(title: string | null) {
return html` <a
class="configuration-container"
role="option"
href="/config"
data-panel="config"
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item class="configuration" role="option">
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
${!this.alwaysExpand && this._updatesCount > 0
? html`
<span class="configuration-badge" slot="item-icon">
${this._updatesCount}
</span>
`
: ""}
<span class="item-text">${title}</span>
${this.alwaysExpand && this._updatesCount > 0
? html`
<span class="configuration-badge">${this._updatesCount}</span>
`
: ""}
</paper-icon-item>
</a>`;
}
private _renderNotifications() {
let notificationCount = this._notifications
const notificationCount = this._notifications
? this._notifications.length
: 0;
for (const entityId in this.hass.states) {
if (computeDomain(entityId) === "configurator") {
notificationCount++;
}
}
return html`<div
class="notifications-container"
@@ -953,18 +999,21 @@ class HaSidebar extends LitElement {
height: 1px;
background-color: var(--divider-color);
}
.notifications-container {
.notifications-container,
.configuration-container {
display: flex;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) .notifications-container {
:host([rtl]) .notifications-container,
:host([rtl]) .configuration-container {
margin-left: initial;
margin-right: env(safe-area-inset-right);
}
.notifications {
cursor: pointer;
}
.notifications .item-text {
.notifications .item-text,
.configuration .item-text {
flex: 1;
}
.profile {
@@ -988,7 +1037,10 @@ class HaSidebar extends LitElement {
margin-right: 8px;
}
.notification-badge {
.notification-badge,
.configuration-badge {
left: calc(var(--app-drawer-width) - 42px);
position: absolute;
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;
@@ -999,7 +1051,11 @@ class HaSidebar extends LitElement {
padding: 0px 6px;
color: var(--text-accent-color, var(--text-primary-color));
}
ha-svg-icon + .notification-badge {
.configuration-badge {
background-color: var(--primary-color);
}
ha-svg-icon + .notification-badge,
ha-svg-icon + .configuration-badge {
position: absolute;
bottom: 14px;
left: 26px;

View File

@@ -43,6 +43,7 @@ import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
import "./ha-area-picker";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-input-helper-text";
@customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) {
@@ -52,6 +53,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property() public helper?: string;
/**
* Show only targets with entities from specific domains.
* @type {Array}
@@ -213,7 +216,11 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
</span>
</span>
</div>
</div>`;
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""} `;
}
private async _showPicker(ev) {
@@ -609,6 +616,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
:host-context([style*="direction: rtl;"]) .mdc-chip__icon {
margin-right: -14px !important;
margin-left: 4px !important;
}
`;
}
}

View File

@@ -19,13 +19,11 @@ export class HaTextArea extends TextAreaBase {
textfieldStyles,
textareaStyles,
css`
:host([autogrow]) {
max-height: 200px;
}
:host([autogrow]) .mdc-text-field {
position: relative;
min-height: 74px;
min-width: 178px;
max-height: 200px;
}
:host([autogrow]) .mdc-text-field:after {
content: attr(data-value);

View File

@@ -91,6 +91,19 @@ export class HaTextField extends TextFieldBase {
.mdc-text-field {
overflow: var(--text-field-overflow);
}
:host-context([style*="direction: rtl;"]) .mdc-floating-label {
right: 10px !important;
left: initial !important;
}
:host-context([style*="direction: rtl;"])
.mdc-text-field--with-leading-icon.mdc-text-field--filled
.mdc-floating-label {
max-width: calc(100% - 48px);
right: 48px !important;
left: initial !important;
}
`,
];
}

View File

@@ -2,29 +2,31 @@ import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import "../../../components/ha-select";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { HomeAssistant } from "../types";
import "./ha-select";
@customElement("hui-theme-select-editor")
export class HuiThemeSelectEditor extends LitElement {
@customElement("ha-theme-picker")
export class HaThemePicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
protected render(): TemplateResult {
return html`
<ha-select
.label=${this.label ||
`${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`}
this.hass!.localize("ui.components.theme_picker.theme")}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
@@ -32,7 +34,7 @@ export class HuiThemeSelectEditor extends LitElement {
>
<mwc-list-item value="remove"
>${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.no_theme"
"ui.components.theme_picker.no_theme"
)}</mwc-list-item
>
${Object.keys(this.hass!.themes.themes)
@@ -64,6 +66,6 @@ export class HuiThemeSelectEditor extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hui-theme-select-editor": HuiThemeSelectEditor;
"ha-theme-picker": HaThemePicker;
}
}

View File

@@ -2,8 +2,8 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { useAmPm } from "../common/datetime/use_am_pm";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-base-time-input";
import { FrontendLocaleData } from "../data/translation";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
@customElement("ha-time-input")
@@ -14,8 +14,12 @@ export class HaTimeInput extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "enable-second" })
public enableSecond = false;
@@ -43,6 +47,8 @@ export class HaTimeInput extends LitElement {
.disabled=${this.disabled}
@value-changed=${this._timeChanged}
.enableSecond=${this.enableSecond}
.required=${this.required}
.helper=${this.helper}
></ha-base-time-input>
`;
}

38
src/components/ha-tip.ts Normal file
View File

@@ -0,0 +1,38 @@
import { mdiLightbulbOutline } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "./ha-svg-icon";
@customElement("ha-tip")
class HaTip extends LitElement {
public render() {
return html`
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
<span class="prefix">Tip!</span>
<span class="text"><slot></slot></span>
`;
}
static styles = css`
:host {
display: block;
text-align: center;
}
.text {
margin-left: 2px;
color: var(--secondary-text-color);
}
.prefix {
font-weight: 500;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-tip": HaTip;
}
}

View File

@@ -33,6 +33,8 @@ export class HaYamlEditor extends LitElement {
@property({ type: Boolean }) public readOnly = false;
@property({ type: Boolean }) public required = false;
@state() private _yaml = "";
public setValue(value): void {
@@ -59,7 +61,9 @@ export class HaYamlEditor extends LitElement {
return html``;
}
return html`
${this.label ? html`<p>${this.label}</p>` : ""}
${this.label
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
: ""}
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}

View File

@@ -19,6 +19,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import type { HomeAssistant } from "../../types";
import "../ha-input-helper-text";
import "./ha-map";
import type { HaMap } from "./ha-map";
@@ -50,6 +51,8 @@ export class HaLocationsEditor extends LitElement {
@property({ attribute: false }) public locations?: MarkerLocation[];
@property() public helper?: string;
@property({ type: Boolean }) public autoFit = false;
@property({ type: Number }) public zoom = 16;
@@ -102,13 +105,18 @@ export class HaLocationsEditor extends LitElement {
}
protected render(): TemplateResult {
return html`<ha-map
.hass=${this.hass}
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
.darkMode=${this.darkMode}
></ha-map>`;
return html`
<ha-map
.hass=${this.hass}
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
.darkMode=${this.darkMode}
></ha-map>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}
private _getLayers = memoizeOne(
@@ -287,11 +295,8 @@ export class HaLocationsEditor extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
height: 300px;
}
ha-map {
display: block;
height: 100%;
}
`;

View File

@@ -488,6 +488,14 @@ export class HaMap extends ReactiveElement {
text-align: center;
color: var(--primary-text-color);
}
.leaflet-pane {
z-index: 0 !important;
}
.leaflet-control,
.leaflet-top,
.leaflet-bottom {
z-index: 1 !important;
}
`;
}
}

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