Compare commits

..

168 Commits

Author SHA1 Message Date
Ludeeus
a6f8f0495c Do not show skip if entity support auto update 2022-03-31 16:12:48 +00: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
Zack Barett
c7e9ee785d Merge pull request #12109 from home-assistant/number_selector_allow_0 2022-03-23 08:42:00 -05:00
Erik
079cc39a6e Fix selecting 0 with number selector 2022-03-23 14:24:55 +01:00
Marc Mueller
d6a1d5af79 Remove setup.py (#11593) 2022-03-23 09:22:12 +01:00
Matthias de Baat
c0dce08e19 Create user types page and rename the category (#12089)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-23 03:51:35 +00:00
Zack Barett
a7a347ed05 Bumped version to 20220322.0 (#12102) 2022-03-22 17:08:30 -07:00
Zack Barett
2d9b50defc Fix Duration Selector Default (#12098)
* Fix Duration Default

* USe initial form data function
2022-03-22 23:33:16 +00:00
Paulus Schoutsen
840858b18c Add statistic adjust dialog (#12101)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-22 22:40:00 +00:00
Zack Barett
afd2e71f6c change from hidden to not shown (#12097) 2022-03-22 15:39:22 -07:00
Zack Barett
88af0aa788 Add entity include and exclude to selector (#12078)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-22 19:58:03 +00:00
Zack Barett
49124f6f09 Update When entity can change enabled or hidden (#12096) 2022-03-22 19:53:22 +00:00
Paulus Schoutsen
73f5580555 Add support for integration type (#12077) 2022-03-22 14:47:12 -05:00
Joakim Sørensen
bdde5268c6 Add support for update entities (#12059)
* Add support for update entities

* Apply suggestions from code review

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

* Add to gallery

* implement xx%

* Adjustments for skipped

* Add progress bar

* Add UPDATE_SUPPORT_INSTALL

* Allow skipping without install support

* Add version to service call if supported

* Adjust changelog link

* Use Installing

* adjustments

* Use unavailable

Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-22 10:23:54 -07:00
Zack Barett
15e972c158 Stack Action Inputs in the Button Editor (#12076)
* Stack Action Inputs in the Button Editor

* update style

* Update for other editors
2022-03-22 08:57:09 -07:00
Zack Barett
0fc4c24f5a Merge pull request #12087 from DuckyCrayfish/fix-toolbar-styles 2022-03-22 10:29:11 -05:00
Zack Barett
9eba50df0c Merge pull request #11651 from home-assistant/add-docs-icon-config-flow 2022-03-22 10:23:43 -05:00
Zack Barett
0e0e07437f Update src/dialogs/config-flow/dialog-data-entry-flow.ts 2022-03-22 10:08:43 -05:00
Joakim Sørensen
6ac51ede52 Change Netlify preview URL (#12095) 2022-03-22 16:00:43 +01:00
Pawel
ccf1fb573a Fix gas energy graph units if stats added by external source (#11892) 2022-03-21 21:15:28 -07:00
Nick Iacullo
fa537968c4 Update styles for hui-editor
Update the background-color and text-color of the app-toolbar in
hui-editor to match the styles of hui-root while in edit-mode.

Previously, these properties were set using undefined css variables that
could not be changed via themes (--dark-background-color and
--dark-text-color).
2022-03-21 10:28:24 +00:00
Marc Mueller
6bf2111a3c Upload release assets (#11566)
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2022-03-21 08:23:05 +01:00
Michael Irigoyen
f5f8be8276 Update required version of MDI to 6.6.95 2022-03-20 23:32:21 -05:00
J. Nick Koston
ddf1cc0733 Fetch history with no_attributes for entities that do not need them (#12082) 2022-03-20 15:47:13 -10:00
Zack Barett
9c1d1cb6f6 Merge pull request #12069 from matthiasdebaat/patch-2 2022-03-18 07:32:14 -05:00
Matthias de Baat
470225abde Update logo.markdown 2022-03-18 11:26:19 +01:00
Matthias de Baat
ee230b86c1 Update gallery/src/pages/brand/logo.markdown
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-18 11:25:41 +01:00
Matthias de Baat
f927fc64a9 Update gallery/src/pages/brand/logo.markdown
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-18 11:25:34 +01:00
Zack Barett
03677c33f7 Bumped version to 20220317.0 (#12074) 2022-03-17 15:27:27 -07:00
Zack Barett
bc36a206da Merge pull request #11984 from Mariusthvdb/patch-4 2022-03-17 17:15:41 -05:00
Zack Barett
af06ab1e2d Merge pull request #12073 from home-assistant/bump-haws-701 2022-03-17 17:11:53 -05:00
Zack Barett
3e2135a485 Add radio Form Logic to Select Selector (#12063) 2022-03-17 15:04:59 -07:00
Zack Barett
2e7f8fb46f Add Date Time Selector (#12070) 2022-03-17 15:01:08 -07:00
Paulus Schoutsen
102568c4bd Update lock 2022-03-17 14:58:45 -07:00
Paulus Schoutsen
4fcdae842e Bump HAWS to 7.0.1 2022-03-17 14:50:16 -07:00
Paulus Schoutsen
ea19740f5a Ignore diagnostics not found exceptions (#12066) 2022-03-17 13:44:06 -07:00
Matthias de Baat
3e0942b631 Add files via upload 2022-03-17 14:19:28 +01:00
Matthias de Baat
0261cea796 Create new Logo page 2022-03-17 14:14:13 +01:00
Paulus Schoutsen
5247b2813f Bump HAWS to 7.0.0 (#12067) 2022-03-16 19:54:25 -10:00
Zack Barett
8a5090684e Merge pull request #12064 from home-assistant/20220316.0 2022-03-16 17:59:26 -05:00
Zack Barett
1784ba5e68 Merge pull request #12061 from home-assistant/Add-Date-Selector 2022-03-16 17:52:40 -05:00
Zack Barett
4fbe9a7b10 Merge pull request #12049 from home-assistant/hide-hidden-entities 2022-03-16 17:49:20 -05:00
Zack
1ca9c7838a Bumped version to 20220316.0 2022-03-16 17:46:32 -05:00
Zack
4fc2c3ef05 Remvoe redunency 2022-03-16 17:42:11 -05:00
Zack Barett
73ff8e28a8 Add Devices Picker (#12056) 2022-03-16 15:40:34 -07:00
Zack
dde1c5e03c Entity Status 2022-03-16 17:38:38 -05:00
Zack
01eed22592 clean up 2022-03-16 17:34:09 -05:00
Zack
94ebb63589 add to basic editor and update advanced style 2022-03-16 17:25:08 -05:00
Zack
29119db5ce Add translation 2022-03-16 17:05:52 -05:00
Paulus Schoutsen
9908162ac2 Add support for menu data entry flow option (#12055) 2022-03-16 14:14:38 -07:00
Paulus Schoutsen
1e929ae78a Revamp URL form (#12060) 2022-03-16 14:14:25 -07:00
Paulus Schoutsen
ab5df0fe6e test condition (#11925) 2022-03-16 14:13:13 -07:00
Paulus Schoutsen
d5010dda9e Add ha-form context (#12062) 2022-03-16 14:12:10 -07:00
Zack
4ac097f32b Add Date Selector 2022-03-16 14:20:45 -05:00
Zack Barett
5d3d15072f Merge pull request #12054 from home-assistant/Add-image-to-design-docs 2022-03-16 13:24:03 -05:00
Zack Barett
5c53bc4225 Add Color RGB Selector (#12039) 2022-03-15 15:34:02 -07:00
Zack
d5a307f8f4 Add icons and buttons 2022-03-15 15:00:35 -05:00
Zack
a27dd1e7f1 Add Description of chosen 2022-03-15 14:47:15 -05:00
Zack
c86ed1fb3e remove 1 2022-03-15 14:33:11 -05:00
Zack
7fa7a48072 Disabled by 2022-03-15 14:32:49 -05:00
Zack
4e0fc8ee08 Update Translations 2022-03-15 14:26:21 -05:00
Zack
5f6490e54e Add HA to public folder and show in markdown 2022-03-15 14:17:24 -05:00
Matthias de Baat
db78b046a2 Add Brand folder and Our story page (#11978)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-15 09:01:42 -05:00
Zack
c37fe1e7ff add to demo 2022-03-14 20:39:03 -05:00
Zack
f1ec479d41 Reviews 2022-03-14 20:37:37 -05:00
Zack
e01cb3ca82 Utilize Hide Hidden Entities 2022-03-14 14:22:45 -05:00
Zack Barett
b8d3c68a7a Add Color Temp Selector (#12041) 2022-03-14 11:07:15 -07:00
Matthias de Baat
641003bb2a Rename Lovelace Dashboard to just Dashboard (#12044)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-03-14 11:05:44 -07:00
Zack Barett
3358fc2b18 Add all cover device classes (#12042) 2022-03-14 10:45:12 -07:00
Zack Barett
dcf50e055b Fix @changed where using ev.detail (#12043) 2022-03-14 16:11:46 +00:00
Zack Barett
1fa04baa16 Fix: Changing Blueprint Automation Name (#12036) 2022-03-14 08:33:49 -07:00
jpearl
84ffa2369a Add shade to device class overrides (#11874) 2022-03-14 10:19:43 -05:00
Paulus Schoutsen
cc27ddb362 Bumped version to 20220312.0 2022-03-12 13:47:05 -08:00
Paulus Schoutsen
c4dc6bfb0d Bumped version to 20220301.2 2022-03-12 13:45:34 -08:00
Paulus Schoutsen
4fbcc30a37 Merge remote-tracking branch 'origin/master' into dev 2022-03-12 13:43:33 -08:00
Paulus Schoutsen
4916527e5f Bumped version to 20220301.1 2022-03-12 13:42:42 -08:00
Paulus Schoutsen
fad8a27232 HAWS 6.1 (#12016) 2022-03-12 09:56:25 -10:00
Zack Barett
a993d3a753 Script ID update with Alias (#12008) 2022-03-11 21:25:09 -08:00
Zack Barett
5dfe17a43a Fix: Allow for deleting Input_select options (#12007) 2022-03-11 17:07:56 -08:00
Zack Barett
9b6c935ffb Fix For Selecting Device Class (#12010) 2022-03-11 09:39:04 -08:00
Zack Barett
f4e28da0a3 Fix Dashboard Editing (#12011) 2022-03-11 09:38:18 -08:00
Zack Barett
294a69d7e4 Fix changing cost number in energy settings (#12009) 2022-03-11 09:37:22 -08:00
Charles Garwood
f89b8cffcf Fix zwave_js set config dropdown default value (#11974)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-03-10 09:21:04 -06:00
Raman Gupta
99fd3a1b6f Fix zwave_js 'add/remove device' disabled bug (#12000)
* Fix zwave_js 'add/remove device' disabled bug

* revert extra change
2022-03-10 08:45:12 -05:00
Emil Stjerneman
246e426182 #11971 Change order of alarm panel buttons (#11998) 2022-03-09 19:54:40 -06:00
Paulus Schoutsen
9f1e9b43fe Use entities-picker in entity selector (#11990) 2022-03-08 21:33:23 -06:00
Marius
8301ae262c change icon to mimic physical device
and follow comments
2022-03-08 21:40:42 +01:00
Zack Barett
d968fe41ee Update Style of Design Page (#11982) 2022-03-08 10:19:18 -08:00
Bram Kragten
db830e9014 Fix theme setting (#11977) 2022-03-08 10:13:08 -08:00
Paulus Schoutsen
fc6b594a27 Allow selecting multiple entities (#11986) 2022-03-08 10:09:45 -08:00
Marius
86dbf99ebe replace default switch icon
to  make it stand out against a power entity which uses the same mdiFlash https://github.com/home-assistant/core/issues/67620#issuecomment-1061949527

suggest the Outline version, so create a subtle difference with the on/off icons.
2022-03-08 17:41:32 +01:00
Joakim Sørensen
68e7ce1883 Add systemd_resolved unsupported reason (#11971) 2022-03-07 17:42:49 +01:00
Bram Kragten
4cdff3faea Add location selector, convert zone editor (#11902) 2022-03-07 08:47:20 -06:00
Bram Kragten
0dac10aa23 Convert file upload to mdc (#11906) 2022-03-07 08:42:40 -06:00
Bram Kragten
4b8b14a69d A11y expansion panel (#11967) 2022-03-07 08:40:19 -06:00
Zack Barett
9d28df31bd Fix for Statistics Editor (#11942)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-03-07 15:29:00 +01:00
Bram Kragten
8258641443 Make min width of select configurable (#11965) 2022-03-07 14:55:44 +01:00
Bram Kragten
dfcb0f6ba0 Fix humidifier more info mode dropdown (#11964) 2022-03-07 07:25:38 -06:00
Philip Allgaier
2e10eb04b6 Correct media upload error + add file name (#11949)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-03-07 12:08:54 +00:00
Raman Gupta
b4b52d3872 Remove some additional old zwave code (#11941) 2022-03-07 12:49:51 +01:00
Bram Kragten
3873203721 Convert inputs (#11907)
* Convert inputs

* Update dialog-thingtalk.ts

* imports
2022-03-07 12:45:39 +01:00
Paulus Schoutsen
ccb91e0b49 Allow marking YAML editor as read only (#11960) 2022-03-07 12:39:16 +01:00
Paulus Schoutsen
bd20c15a55 Show triggered vars on click (#11924) 2022-03-04 23:24:31 -08:00
Paulus Schoutsen
0936fd9ae4 Guard setting up config flow for an unsupported domain (#11937) 2022-03-04 14:31:11 -08:00
Bram Kragten
adefc7a4e2 Convert lovelace config dialogs to ha-form (#11910) 2022-03-04 23:15:10 +01:00
Bram Kragten
8f8017ecff Remove zwave and ozw panels (#11911)
Remove zwave and ozw panels
2022-03-04 14:10:44 -08:00
Robin Wittebol
604b79696e Always show tab labels (#11919) 2022-03-03 19:46:14 +01:00
Robin Wittebol
8c445f6409 Fix datepicker triangle (#11920) 2022-03-03 19:45:03 +01:00
Bram Kragten
797c871137 Convert objects to string in config flow error (#11908) 2022-03-03 13:55:40 +01:00
Steve Repsher
24829bd903 Supervisor mobile click accessibility (#11915) 2022-03-03 10:15:22 +01:00
Bram Kragten
add92a559d Fix quickbar overlaying, fix click handling (#11900) 2022-03-02 17:50:01 +01:00
Paulus Schoutsen
35a41b3490 Use same help icon everywhere 2022-02-11 08:35:29 -08:00
Paulus Schoutsen
f59cb661cd Add a docs icon to the config flow dialog 2022-02-10 14:27:38 -08:00
220 changed files with 6572 additions and 7517 deletions

View File

@@ -10,10 +10,18 @@ env:
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
# All scopes not mentioned here are set to no access
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
permissions:
actions: none
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v2
@@ -47,6 +55,13 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@v0.1.14
with:
files: |
dist/*.whl
dist/*.tar.gz
wheels-init:
name: Init wheels build
needs: release

View File

@@ -1,4 +1,3 @@
import "web-animations-js/web-animations-next-lite.min";
import "../../../src/resources/ha-style";
import "../../../src/resources/roboto";
import "./layout/hc-lovelace";

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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -23,7 +23,7 @@ if [[ "${PULL_REQUEST}" == "true" ]]; then
createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
gulp build-gallery
if [ $? -eq 0 ]; then
createStatus "success" "Build complete" "$DEPLOY_URL"
createStatus "success" "Build complete" "$DEPLOY_PRIME_URL"
else
createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
fi

View File

@@ -36,12 +36,17 @@ module.exports = [
category: "misc",
header: "Miscelaneous",
},
{
category: "brand",
header: "Brand",
},
{
category: "user-test",
header: "User Tests",
header: "Users",
pages: ["user-types", "configuration-menu"],
},
{
category: "design.home-assistant.io",
header: "Design Documentation",
header: "About",
},
];

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) {

View File

@@ -12,7 +12,14 @@ class PageDescription extends HaMarkdown {
if (!PAGES[this.page].description) {
return html``;
}
return html`
<div class="heading">
<div class="title">
${PAGES[this.page].metadata.title || this.page.split("/")[1]}
</div>
<div class="subtitle">${PAGES[this.page].metadata.subtitle}</div>
</div>
${until(
PAGES[this.page]
.description()
@@ -25,9 +32,22 @@ class PageDescription extends HaMarkdown {
static styles = [
HaMarkdown.styles,
css`
.heading {
padding: 16px;
border-bottom: 1px solid var(--secondary-background-color);
}
.title {
font-size: 42px;
line-height: 56px;
padding-bottom: 8px;
}
.subtitle {
font-size: 18px;
line-height: 24px;
}
.root {
max-width: 800px;
margin: 0 auto;
margin: 16px auto;
}
.root > *:first-child {
margin-top: 0;

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

@@ -5,6 +5,7 @@ import { html, css, LitElement, PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../src/components/ha-icon-button";
import "../../src/managers/notification-manager";
import "../../src/components/ha-expansion-panel";
import { haStyle } from "../../src/resources/styles";
import { PAGES, SIDEBAR } from "../build/import-pages";
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
@@ -44,6 +45,10 @@ class HaGallery extends LitElement {
for (const page of group.pages!) {
const key = `${group.category}/${page}`;
const active = this._page === key;
if (!(key in PAGES)) {
console.error("Undefined page referenced in sidebar.js:", key);
continue;
}
const title = PAGES[key].metadata.title || page;
links.push(html`
<a ?active=${active} href=${`#${group.category}/${page}`}>${title}</a>
@@ -53,10 +58,9 @@ class HaGallery extends LitElement {
sidebar.push(
group.header
? html`
<details>
<summary class="section">${group.header}</summary>
<ha-expansion-panel .header=${group.header}>
${links}
</details>
</ha-expansion-panel>
`
: links
);
@@ -92,27 +96,34 @@ class HaGallery extends LitElement {
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
</div>
<div class="page-footer">
${PAGES[this._page].description ||
Object.keys(PAGES[this._page].metadata).length > 0
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
target="_blank"
>
Edit text
</a>
`
: ""}
${PAGES[this._page].demo
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
target="_blank"
>
Edit demo
</a>
`
: ""}
<div class="header">Help us to improve our documentation</div>
<div class="secondary">
Suggest an edit to this page, or provide/view feedback for this
page.
</div>
<div>
${PAGES[this._page].description ||
Object.keys(PAGES[this._page].metadata).length > 0
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
target="_blank"
>
Edit text
</a>
`
: ""}
${PAGES[this._page].demo
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
target="_blank"
>
Edit demo
</a>
`
: ""}
</div>
</div>
</div>
</mwc-drawer>
@@ -186,27 +197,16 @@ class HaGallery extends LitElement {
padding: 4px;
}
.sidebar details {
margin-top: 1em;
margin-left: 1em;
}
.sidebar summary {
cursor: pointer;
font-weight: bold;
margin-bottom: 8px;
}
.sidebar a {
color: var(--primary-text-color);
display: block;
padding: 4px 12px;
padding: 12px;
text-decoration: none;
position: relative;
}
.sidebar a[active]::before {
border-radius: 4px;
border-radius: 12px;
position: absolute;
top: 0;
right: 2px;
@@ -237,14 +237,32 @@ class HaGallery extends LitElement {
.page-footer {
text-align: center;
margin: 16px 0;
padding-top: 16px;
border-top: 1px solid rgba(0, 0, 0, 0.12);
margin: 16px;
padding: 16px;
border-radius: 12px;
background-color: var(--primary-background-color);
}
.page-footer div {
margin-top: 4px;
}
.page-footer .header {
font-size: 16px;
font-weight: 500;
line-height: 28px;
text-align: center;
}
.page-footer .secondary {
line-height: 23px;
text-align: center;
}
.page-footer a {
display: inline-block;
margin: 0 8px;
text-decoration: none;
}
`,
];

View File

@@ -0,0 +1,34 @@
---
title: "Logo"
---
![Using our logo](/images/using-our-logo.png)
# Using our logo
As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color.
[Download Logo](https://github.com/home-assistant/assets/tree/master/logo)
![Logo](/images/logo.png)
## Using the icon
Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon.
![Logo variants](/images/logo-variants.png)
## Using the right variant
The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography.
When needed you can use our logo without a shadow, as seen as the second variant.
The outlined logo should only be used on packaging.
## Exclusion zone
The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon.
![Clearspace](/images/clearspace.png)

View File

@@ -0,0 +1,41 @@
---
title: "Our story"
---
## Open source home automation that puts local control and privacy first
Home Assistant is a free and open-source software for home automation that is designed to be the central control system for smart home devices with a focus on local control and privacy. It can be accessed via a web-based user interface, via apps for Android and iOS, or using voice commands via a supported virtual assistant like Google Assistant and Amazon Alexa.
IoT devices and services are supported by modular support for controlling proprietary ecosystems if they provide public access via an Open API for third-party integrations and protocols like Bluetooth, MQTT, Zigbee, and Z-Wave, After the Home Assistant software application is installed as a computer appliance it will act as a central control system for home automation. Information from all entities it sees can be used and controlled from within scripts trigger automations using scheduling and "blueprint" subroutines, e.g. for controlling lighting, climate, entertainment systems, and appliances.
# Open Home
The Open Home is our vision for the smart home. It defines the values that we put at the heart of every decision we make at Home Assistant. Its woven into our architecture, licensing, community, and everything else.
The Open Home is about privacy, choice, and durability.
## Privacy
Your home should be your safe space. A place where you can be your true self without having to bother about what the world thinks of you. A place where you dont need to act differently to avoid an algorithm categorizing your behavior. Privacy for the Open Home means that devices need to work locally. No one else needs to know if you turn on a light bulb or change the thermostat.
It is okay for a product to offer a cloud connection, but it should be extra and opt-in.
## Choice
Devices in your home gather data about themselves and their surroundings. Your data. Vendors shouldnt be able to limit your access to your data or limit the interoperability of your devices with the rest of your smart home.
Choice for the Open Home means that devices need to make the gathered data available through local APIs. This avoids vendor lock-in and allows users to create their own smart home with devices from different manufacturers.
## Durability
If there is one thing that technology firms are very good at, it is launching new products. However, maintaining the products and making sure they keep working is an afterthought for most. The result is that vendors can decide to no longer support your device, crippling its features or even preventing it from working at all. As we install more and more devices in our home, durability is becoming more and more important. We shouldnt have to buy everything new every couple of years because the manufacturer decided to move on.
Durability for the Open Home means that devices are designed and built to keep working. Not just this year, but for the next decade.
# Our history
The project was started as a Python application by Paulus Schoutsen in September 2013 and first published publicly on GitHub in November 2013. In July 2017, a managed operating system called Hass.io was initially introduced to make it easier use to use Home Assistant on single-board computers like the Raspberry Pi series. Its bundled "supervisor" management system allowed users to manage, backup, and update the local installation and introduced the option to extend the functionality of the software with add-ons.
An optional subscription service was introduced in December 2017 for $5/month to solve the complexities associated with secured remote access, as well as linking to Amazon Alexa and Google Assistant. Nabu Casa, Inc. was formed in September 2018 to take over the subscription service. The company's funding is based solely on revenue from the subscription service. It is used to finance the project's infrastructure and to pay for full-time employees contributing to the project.
In January 2020, branding was adjusted to make it easier to refer to different parts of the project. The main piece of software was renamed to Home Assistant Core, while the full suite of software with the embedded operating system and bundled "supervisor" management system was renamed to Home Assistant.

View File

@@ -1,5 +1,6 @@
---
title: Alerts
subtitle: An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task.
---
# Alert `<ha-alert>`

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,17 +1,109 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, html } from "lit";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-form/ha-form";
import "../../components/demo-black-white-row";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
friendly_name: "Alarm",
}),
getEntity("media_player", "livingroom", "playing", {
friendly_name: "Livingroom",
}),
getEntity("media_player", "lounge", "idle", {
friendly_name: "Lounge",
supported_features: 444983,
}),
getEntity("light", "bedroom", "on", {
friendly_name: "Bedroom",
}),
getEntity("switch", "coffee", "off", {
friendly_name: "Coffee",
}),
];
const DEVICES = [
{
area_id: "bedroom",
configuration_url: null,
config_entries: ["config_entry_1"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_1",
identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: null,
name: "Dishwasher",
sw_version: null,
hw_version: null,
via_device_id: null,
},
{
area_id: "backyard",
configuration_url: null,
config_entries: ["config_entry_2"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_2",
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: null,
name: "Lamp",
sw_version: null,
hw_version: null,
via_device_id: null,
},
{
area_id: null,
configuration_url: null,
config_entries: ["config_entry_3"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_3",
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: "User name",
name: "Technical name",
sw_version: null,
hw_version: null,
via_device_id: null,
},
];
const AREAS = [
{
area_id: "backyard",
name: "Backyard",
picture: null,
},
{
area_id: "bedroom",
name: "Bedroom",
picture: null,
},
{
area_id: "livingroom",
name: "Livingroom",
picture: null,
},
];
const SCHEMAS: {
title: string;
@@ -39,6 +131,7 @@ const SCHEMAS: {
icon: "Icon",
media: "Media",
location: "Location",
entities: "Entities",
},
schema: [
{ name: "addon", selector: { addon: {} } },
@@ -46,6 +139,7 @@ const SCHEMAS: {
{
name: "Attribute",
selector: { attribute: { entity_id: "" } },
context: { filter_entity: "entity" },
},
{ name: "Device", selector: { device: {} } },
{ name: "Duration", selector: { duration: {} } },
@@ -53,7 +147,9 @@ const SCHEMAS: {
{ name: "target", selector: { target: {} } },
{ name: "number", selector: { number: { min: 0, max: 10 } } },
{ name: "boolean", selector: { boolean: {} } },
{ name: "time", selector: { time: {} } },
{ name: "time", required: true, selector: { time: {} } },
{ name: "datetime", required: true, selector: { datetime: {} } },
{ name: "date", required: true, selector: { date: {} } },
{ name: "action", selector: { action: {} } },
{ name: "text", selector: { text: { multiline: false } } },
{ name: "text_multiline", selector: { text: { multiline: true } } },
@@ -80,6 +176,10 @@ const SCHEMAS: {
name: "location",
selector: { location: { radius: true, icon: "mdi:home" } },
},
{
name: "entities",
selector: { entity: { multiple: true } },
},
],
},
{
@@ -320,9 +420,10 @@ class DemoHaForm extends LitElement {
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
hass.addEntities(ENTITIES);
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockDeviceRegistry(hass, DEVICES);
mockAreaRegistry(hass, AREAS);
mockHassioSupervisor(hass);
}

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",
@@ -146,6 +146,8 @@ const SCHEMAS: {
},
boolean: { name: "Boolean", selector: { boolean: {} } },
time: { name: "Time", selector: { time: {} } },
date: { name: "Date", selector: { date: {} } },
datetime: { name: "Date Time", selector: { datetime: {} } },
action: { name: "Action", selector: { action: {} } },
text: {
name: "Text",
@@ -162,9 +164,42 @@ const SCHEMAS: {
},
},
object: { name: "Object", selector: { object: {} } },
select_radio: {
name: "Select (Radio)",
selector: {
select: { options: ["Option 1", "Option 2"], mode: "list" },
},
},
select: {
name: "Select",
selector: { select: { options: ["Option 1", "Option 2"] } },
selector: {
select: {
options: [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
"Option 6",
],
},
},
},
select_custom: {
name: "Select (Custom)",
selector: {
select: {
custom_value: true,
options: [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
"Option 6",
],
},
},
},
icon: { name: "Icon", selector: { icon: {} } },
media: { name: "Media", selector: { media: {} } },
@@ -173,6 +208,47 @@ const SCHEMAS: {
name: "Location with radius",
selector: { location: { radius: true, icon: "mdi:home" } },
},
color_temp: {
name: "Color Temperature",
selector: { color_temp: {} },
},
color_rgb: { name: "Color", selector: { color_rgb: {} } },
},
},
{
name: "Multiples",
input: {
entity: { name: "Entity", selector: { entity: { multiple: true } } },
device: { name: "Device", selector: { device: { multiple: true } } },
area: { name: "Area", selector: { area: { multiple: true } } },
select: {
name: "Select Multiple",
selector: {
select: {
multiple: true,
custom_value: true,
options: [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
"Option 6",
],
},
},
},
select_checkbox: {
name: "Select Multiple (Checkbox)",
required: false,
selector: {
select: {
mode: "list",
multiple: true,
options: ["Option 1", "Option 2", "Option 3", "Option 4"],
},
},
},
},
},
];
@@ -181,6 +257,12 @@ const SCHEMAS: {
class DemoHaSelector extends LitElement implements ProvideHassElement {
@state() public hass!: HomeAssistant;
@state() private _disabled = false;
@state() private _required = false;
@state() private _label = true;
private data = SCHEMAS.map(() => ({}));
constructor() {
@@ -314,6 +396,29 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
protected render(): TemplateResult {
return html`
<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>
</div>
${SCHEMAS.map((info, idx) => {
const data = this.data[idx];
const valueChanged = (ev) => {
@@ -336,7 +441,10 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
.hass=${this.hass}
.selector=${value!.selector}
.key=${key}
.label=${this._label ? value!.name : undefined}
.value=${data[key] ?? value!.default}
.disabled=${this._disabled}
.required=${this._required}
@value-changed=${valueChanged}
></ha-selector>
</ha-settings-row>
@@ -349,10 +457,20 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
`;
}
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}
static styles = css`
ha-selector {
width: 60;
}
.options {
padding: 16px 48px;
}
.options ha-formfield {
margin-right: 16px;
}
`;
}

View File

@@ -2,6 +2,8 @@
title: Editing design.home-assistant.io
---
![Home Assistant Logo](/images/logo-with-text.png)
# How to edit design.home-assistant.io
All pages are stored in [the pages folder][pages-folder] on GitHub. Pages are grouped in a folder per sidebar section. Each page can contain a `<page name>.markdown` description file, a `<page name>.ts` demo file or both. If both are defined the description is rendered first. The description can contain metadata to specify the title of the page.
@@ -41,15 +43,12 @@ import { html, css, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
@customElement("demo-user-experience-usability")
export class DemoUserExperienceUsability extends LitElement {
protected render() {
return html`
<ha-card>
<div class="card-content">
Hello world!
</div>
<div class="card-content">Hello world!</div>
</ha-card>
`;
}

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

@@ -188,6 +188,7 @@ const createEntityRegistryEntries = (
device_id: "mock-device-id",
area_id: null,
disabled_by: null,
hidden_by: null,
entity_category: null,
entity_id: "binary_sensor.updater",
name: null,

View File

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

View File

@@ -0,0 +1,185 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import {
UPDATE_SUPPORT_BACKUP,
UPDATE_SUPPORT_PROGRESS,
UPDATE_SUPPORT_INSTALL,
UPDATE_SUPPORT_RELEASE_NOTES,
UPDATE_SUPPORT_AUTO_UPDATE,
} from "../../../../src/data/update";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import {
MockHomeAssistant,
provideHass,
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
import { LONG_TEXT } from "../../data/text";
const base_attributes = {
title: "Awesome",
current_version: "1.2.2",
latest_version: "1.2.3",
release_url: "https://home-assistant.io",
supported_features: UPDATE_SUPPORT_INSTALL,
skipped_version: null,
in_progress: false,
release_summary:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In nec metus aliquet, porta mi ut, ultrices odio. Etiam egestas orci tellus, non semper metus blandit tincidunt. Praesent elementum turpis vel tempor pharetra. Sed quis cursus diam. Proin sem justo.",
};
const ENTITIES = [
getEntity("update", "update1", "on", {
...base_attributes,
friendly_name: "Update",
}),
getEntity("update", "update2", "on", {
...base_attributes,
title: null,
friendly_name: "Update without title",
}),
getEntity("update", "update3", "on", {
...base_attributes,
release_url: null,
friendly_name: "Update without release_url",
}),
getEntity("update", "update4", "on", {
...base_attributes,
release_summary: null,
friendly_name: "Update without release_summary",
}),
getEntity("update", "update5", "off", {
...base_attributes,
current_version: "1.2.3",
friendly_name: "No update",
}),
getEntity("update", "update6", "off", {
...base_attributes,
skipped_version: "1.2.3",
friendly_name: "Skipped version",
}),
getEntity("update", "update7", "on", {
...base_attributes,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_BACKUP,
friendly_name: "With backup support",
}),
getEntity("update", "update8", "on", {
...base_attributes,
in_progress: true,
friendly_name: "With true in_progress",
}),
getEntity("update", "update9", "on", {
...base_attributes,
in_progress: 25,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 25 in_progress",
}),
getEntity("update", "update10", "on", {
...base_attributes,
in_progress: 50,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 50 in_progress",
}),
getEntity("update", "update11", "on", {
...base_attributes,
in_progress: 75,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 75 in_progress",
}),
getEntity("update", "update12", "unavailable", {
...base_attributes,
in_progress: 50,
friendly_name: "Unavailable",
}),
getEntity("update", "update13", "on", {
...base_attributes,
supported_features: 0,
friendly_name: "No install support",
}),
getEntity("update", "update14", "off", {
...base_attributes,
current_version: null,
friendly_name: "Update without current_version",
}),
getEntity("update", "update15", "off", {
...base_attributes,
latest_version: null,
friendly_name: "Update without latest_version",
}),
getEntity("update", "update16", "off", {
...base_attributes,
friendly_name: "Update with release notes",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}),
getEntity("update", "update17", "off", {
...base_attributes,
friendly_name: "Update with release notes error",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}),
getEntity("update", "update18", "off", {
...base_attributes,
friendly_name: "Update with release notes loading",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}),
getEntity("update", "update18", "on", {
...base_attributes,
friendly_name: "Update with auto update",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_AUTO_UPDATE,
}),
];
@customElement("demo-more-info-update")
class DemoMoreInfoUpdate extends LitElement {
@property() public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
hass.mockWS(
"update/release_notes",
(msg: { type: string; entity_id: string }) => {
if (msg.entity_id === "update.update16") {
return LONG_TEXT;
}
if (msg.entity_id === "update.update17") {
return Promise.reject({
code: "error",
message: "Could not fetch release notes",
});
}
if (msg.entity_id === "update.update18") {
return undefined;
}
return null;
}
);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-update": DemoMoreInfoUpdate;
}
}

View File

@@ -0,0 +1,17 @@
---
title: "User types"
---
We have defined three user types for Home Assistant. They are a lean segmentation of users that helps us make decisions throughout the product. User types differ from traditional personas in that the segmentation criteria arent demographic and dont personify a group into a single character with a fictitious background story.
# Outgrowers
Users that outgrow big tech smart home solutions. It just needs to work with easy setup via an app.
# Tinkerers
Technoid users in home networking and development that know how to code.
# Questioner
Users who want more advanced home automation, but need support to make it work.

View File

@@ -1,9 +1,12 @@
// Compat needs to be first import
import "../../src/resources/compatibility";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import "../../src/resources/roboto";
import "../../src/resources/safari-14-attachshadow-patch";
import "./hassio-main";
setCancelSyntheticClickEvents(false);
const styleEl = document.createElement("style");
styleEl.innerHTML = `
body {

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,14 +72,13 @@
"@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.5.95",
"@mdi/svg": "6.5.95",
"@mdi/js": "6.6.95",
"@mdi/svg": "6.6.95",
"@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",
"@polymer/iron-input": "^3.0.1",
"@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.2.0",
"@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1",
@@ -109,7 +108,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.1.5",
"home-assistant-js-websocket": "^6.0.1",
"home-assistant-js-websocket": "^7.0.1",
"idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0",
@@ -136,7 +135,6 @@
"vis-network": "^8.5.4",
"vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2",
"workbox-cacheable-response": "^6.4.2",
"workbox-core": "^6.4.2",
"workbox-expiration": "^6.4.2",

View File

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

View File

@@ -1,7 +0,0 @@
"""
Entry point for setuptools. Required for editable installs.
TODO: Remove file after updating to pip 21.3
"""
from setuptools import setup
setup()

View File

@@ -187,6 +187,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"scene",
"sun",
"timer",
"update",
"vacuum",
"water_heater",
"weather",
@@ -200,6 +201,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"input_text",
"number",
"scene",
"update",
"select",
];

View File

@@ -1,12 +1,18 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendLocaleData } from "../../data/translation";
import {
updateIsInstalling,
UpdateEntity,
UPDATE_SUPPORT_PROGRESS,
} from "../../data/update";
import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time";
import { formatNumber, isNumericState } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
export const computeStateDisplay = (
localize: LocalizeFunc,
@@ -130,6 +136,28 @@ export const computeStateDisplay = (
}
}
if (domain === "update") {
// When updating, and entity does not support % show "Installing"
// When updating, and entity does support % show "Installing (xx%)"
// When update available, show the version
// When the latest version is skipped, show the latest version
// When update is not available, show "Up-to-date"
// When update is not available and there is no latest_version show "Unavailable"
return compareState === "on"
? updateIsInstalling(stateObj as UpdateEntity)
? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
? localize("ui.card.update.installing_with_progress", {
progress: stateObj.attributes.in_progress,
})
: localize("ui.card.update.installing")
: stateObj.attributes.latest_version
: stateObj.attributes.skipped_version ===
stateObj.attributes.latest_version
? stateObj.attributes.latest_version ??
localize("state.default.unavailable")
: localize("ui.card.update.up_to_date");
}
return (
// Return device class translation
(stateObj.attributes.device_class &&

View File

@@ -1,4 +1,4 @@
import { HassEntity } from "home-assistant-js-websocket";
import type { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "./compute_domain";
export const computeStateDomain = (stateObj: HassEntity) =>

View File

@@ -9,11 +9,10 @@ import {
mdiCast,
mdiCastConnected,
mdiClock,
mdiEmoticonDead,
mdiFlash,
mdiGestureTapButton,
mdiLanConnect,
mdiLanDisconnect,
mdiLightSwitch,
mdiLock,
mdiLockAlert,
mdiLockClock,
@@ -22,16 +21,16 @@ import {
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiSleep,
mdiTimerSand,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiWeatherNight,
mdiZWave,
mdiPackage,
mdiPackageDown,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { updateIsInstalling, UpdateEntity } from "../../data/update";
/**
* Return the icon to be used for a domain.
*
@@ -112,19 +111,7 @@ export const domainIcon = (
case "switch":
return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
default:
return mdiFlash;
}
case "zwave":
switch (compareState) {
case "dead":
return mdiEmoticonDead;
case "sleeping":
return mdiSleep;
case "initializing":
return mdiTimerSand;
default:
return mdiZWave;
return mdiLightSwitch;
}
case "sensor": {
@@ -149,6 +136,13 @@ export const domainIcon = (
return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain]
: mdiWeatherNight;
case "update":
return compareState === "on"
? updateIsInstalling(stateObj as UpdateEntity)
? mdiPackageDown
: mdiPackageUp
: mdiPackage;
}
if (domain in FIXED_DOMAIN_ICONS) {

View File

@@ -0,0 +1,4 @@
const regexp =
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
export const isIPAddress = (input: string): boolean => regexp.test(input);

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"],
@@ -70,9 +71,6 @@ export const iconColorCSS = css`
}
ha-state-icon[data-domain="plant"][data-state="problem"],
ha-state-icon[data-domain="zwave"][data-state="dead"] {
color: var(--state-icon-error-color);
}
/* Color the icon if unavailable */
ha-state-icon[data-state="unavailable"] {

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

@@ -1,8 +1,9 @@
import "@material/mwc-button";
import type { Button } from "@material/mwc-button";
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import "../ha-circular-progress";
import "../ha-svg-icon";
@customElement("ha-progress-button")
export class HaProgressButton extends LitElement {
@@ -12,38 +13,53 @@ export class HaProgressButton extends LitElement {
@property({ type: Boolean }) public raised = false;
@query("mwc-button", true) private _button?: Button;
@state() private _result?: "success" | "error";
public render(): TemplateResult {
const overlay = this._result || this.progress;
return html`
<mwc-button
?raised=${this.raised}
.disabled=${this.disabled || this.progress}
@click=${this._buttonTapped}
class=${this._result || ""}
>
<slot></slot>
</mwc-button>
${this.progress
? html`<div class="progress">
<ha-circular-progress size="small" active></ha-circular-progress>
</div>`
: ""}
${!overlay
? ""
: html`
<div class="progress">
${this._result === "success"
? html`<ha-svg-icon .path=${mdiCheckBold}></ha-svg-icon>`
: this._result === "error"
? html`<ha-svg-icon .path=${mdiAlertOctagram}></ha-svg-icon>`
: this.progress
? html`
<ha-circular-progress
size="small"
active
></ha-circular-progress>
`
: ""}
</div>
`}
`;
}
public actionSuccess(): void {
this._tempClass("success");
this._setResult("success");
}
public actionError(): void {
this._tempClass("error");
this._setResult("error");
}
private _tempClass(className: string): void {
this._button!.classList.add(className);
private _setResult(result: "success" | "error"): void {
this._result = result;
setTimeout(() => {
this._button!.classList.remove(className);
}, 1000);
this._result = undefined;
}, 2000);
}
private _buttonTapped(ev: Event): void {
@@ -69,6 +85,7 @@ export class HaProgressButton extends LitElement {
background-color: var(--success-color);
transition: none;
border-radius: 4px;
pointer-events: none;
}
mwc-button[raised].success {
@@ -81,6 +98,7 @@ export class HaProgressButton extends LitElement {
background-color: var(--error-color);
transition: none;
border-radius: 4px;
pointer-events: none;
}
mwc-button[raised].error {
@@ -89,13 +107,21 @@ export class HaProgressButton extends LitElement {
}
.progress {
bottom: 0;
margin-top: 4px;
bottom: 4px;
position: absolute;
text-align: center;
top: 0;
top: 4px;
width: 100%;
}
ha-svg-icon {
color: white;
}
mwc-button.success slot,
mwc-button.error slot {
visibility: hidden;
}
`;
}
}

View File

@@ -115,7 +115,7 @@ class DateRangePickerElement extends WrappedElement {
color: var(--primary-text-color);
min-width: initial !important;
}
.daterangepicker:after {
.daterangepicker:before {
display: none;
}
.daterangepicker:after {

View File

@@ -86,6 +86,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
@@ -269,6 +271,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
.value=${this._value}
.renderer=${rowRenderer}
.disabled=${this.disabled}
.required=${this.required}
item-value-path="id"
item-label-path="name"
@opened-changed=${this._openedChanged}

View File

@@ -1,4 +1,4 @@
import { html, LitElement, TemplateResult } from "lit";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
@@ -11,6 +11,8 @@ class HaDevicesPicker extends LitElement {
@property() public value?: string[];
@property({ type: Boolean }) public required?: boolean;
/**
* Show entities from specific domains.
* @type {string}
@@ -66,6 +68,7 @@ class HaDevicesPicker extends LitElement {
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.label=${this.pickDeviceLabel}
.required=${this.required}
@value-changed=${this._addDevice}
></ha-device-picker>
</div>
@@ -116,6 +119,12 @@ class HaDevicesPicker extends LitElement {
this._updateDevices([...currentDevices, toAdd]);
}
static override styles = css`
div {
margin-top: 8px;
}
`;
}
declare global {

View File

@@ -14,6 +14,8 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array }) public value?: string[];
@property({ type: Boolean }) public required?: boolean;
/**
* Show entities from specific domains.
* @type {string}
@@ -46,11 +48,29 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
/**
* List of allowed entities to show. Will ignore all other filters.
* @type {Array}
* @attr include-entities
*/
@property({ type: Array, attribute: "include-entities" })
public includeEntities?: string[];
/**
* List of entities to be excluded.
* @type {Array}
* @attr exclude-entities
*/
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property({ attribute: "picked-entity-label" })
public pickedEntityLabel?: string;
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
@@ -67,6 +87,8 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
.excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter}
@@ -82,10 +104,13 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
.excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter}
.label=${this.pickEntityLabel}
.required=${this.required}
@value-changed=${this._addEntity}
></ha-entity-picker>
</div>
@@ -94,7 +119,9 @@ class HaEntitiesPickerLight extends LitElement {
private _entityFilter: HaEntityPickerEntityFilterFunc = (
stateObj: HassEntity
) => !this.value || !this.value.includes(stateObj.entity_id);
) =>
(!this.value || !this.value.includes(stateObj.entity_id)) &&
(!this.entityFilter || this.entityFilter(stateObj));
private get _currentEntities() {
return this.value || [];

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;
@@ -61,6 +63,7 @@ class HaEntityAttributePicker extends LitElement {
"ui.components.entity.entity-attribute-picker.attribute"
)}
.disabled=${this.disabled || !this.entityId}
.required=${this.required}
.allowCustomValue=${this.allowCustomValue}
item-value-path="value"
item-label-path="label"

View File

@@ -7,6 +7,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-combo-box";
@@ -15,18 +16,21 @@ import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
interface HassEntityWithCachedName extends HassEntity {
friendly_name: string;
}
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassEntity & { friendly_name: string }> =
(item) =>
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span>
</mwc-list-item>`;
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span>
</mwc-list-item>`;
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -35,6 +39,8 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@@ -74,6 +80,22 @@ export class HaEntityPicker extends LitElement {
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
/**
* List of allowed entities to show. Will ignore all other filters.
* @type {Array}
* @attr include-entities
*/
@property({ type: Array, attribute: "include-entities" })
public includeEntities?: string[];
/**
* List of entities to be excluded.
* @type {Array}
* @attr exclude-entities
*/
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) public hideClearIcon = false;
@@ -96,7 +118,7 @@ export class HaEntityPicker extends LitElement {
private _initedStates = false;
private _states: HassEntity[] = [];
private _states: HassEntityWithCachedName[] = [];
private _getStates = memoizeOne(
(
@@ -106,9 +128,11 @@ export class HaEntityPicker extends LitElement {
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
) => {
let states: HassEntity[] = [];
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"]
): HassEntityWithCachedName[] => {
let states: HassEntityWithCachedName[] = [];
if (!hass) {
return [];
@@ -122,7 +146,7 @@ export class HaEntityPicker extends LitElement {
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null },
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
@@ -136,6 +160,30 @@ export class HaEntityPicker extends LitElement {
];
}
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
this.includeEntities!.includes(entityId)
);
return entityIds
.map((key) => ({
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}))
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities!.includes(entityId)
);
}
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
@@ -148,10 +196,17 @@ export class HaEntityPicker extends LitElement {
);
}
states = entityIds.sort().map((key) => ({
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}));
states = entityIds
.map((key) => ({
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}))
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
)
);
if (includeDeviceClasses) {
states = states.filter(
@@ -190,7 +245,7 @@ export class HaEntityPicker extends LitElement {
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null },
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
@@ -228,7 +283,9 @@ export class HaEntityPicker extends LitElement {
this.excludeDomains,
this.entityFilter,
this.includeDeviceClasses,
this.includeUnitOfMeasurement
this.includeUnitOfMeasurement,
this.includeEntities,
this.excludeEntities
);
if (this._initedStates) {
(this.comboBox as any).filteredItems = this._states;
@@ -250,6 +307,7 @@ export class HaEntityPicker extends LitElement {
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
.renderer=${rowRenderer}
.required=${this.required}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}

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")
@@ -30,6 +33,8 @@ class HaAddonPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@query("ha-combo-box") private _comboBox!: HaComboBox;
public open() {
@@ -55,6 +60,8 @@ class HaAddonPicker extends LitElement {
? this.hass.localize("ui.components.addon-picker.addon")
: this.label}
.value=${this._value}
.required=${this.required}
.disabled=${this.disabled}
.renderer=${rowRenderer}
.items=${this._addons}
item-value-path="slug"

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";
@@ -84,6 +84,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@state() private _areas?: AreaRegistryEntry[];
@state() private _devices?: DeviceRegistryEntry[];
@@ -315,6 +317,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
item-label-path="name"
.value=${this.value}
.disabled=${this.disabled}
.required=${this.required}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area")
: this.label}

View File

@@ -0,0 +1,163 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { EntityRegistryEntry } from "../data/entity_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-area-picker";
@customElement("ha-areas-picker")
export class HaAreasPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string[];
@property() public placeholder?: string;
@property({ type: Boolean, attribute: "no-add" })
public noAdd?: boolean;
/**
* Show only areas with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no areas with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only areas with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property({ attribute: "picked-area-label" })
public pickedAreaLabel?: string;
@property({ attribute: "pick-area-label" })
public pickAreaLabel?: string;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const currentAreas = this._currentAreas;
return html`
${currentAreas.map(
(area) => html`
<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}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.required=${this.required}
@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,13 @@
import { LitElement, html, TemplateResult, css } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-select";
import "@material/mwc-list/mwc-list-item";
import "./ha-textfield";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-select";
import "./ha-textfield";
export interface TimeChangedEvent {
days?: number;
hours: number;
minutes: number;
seconds: number;
@@ -21,6 +22,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property() label?: string;
/**
* Helper for the input
*/
@property() helper?: string;
/**
* auto validate time inputs
*/
@@ -41,6 +47,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Boolean }) disabled = false;
/**
* day
*/
@property({ type: Number }) days = 0;
/**
* hour
*/
@@ -61,6 +72,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Number }) milliseconds = 0;
/**
* Label for the day input
*/
@property() dayLabel = "";
/**
* Label for the hour input
*/
@@ -91,6 +107,11 @@ export class HaBaseTimeInput extends LitElement {
*/
@property({ type: Boolean }) enableMillisecond = false;
/**
* show the day field
*/
@property({ type: Boolean }) enableDay = false;
/**
* limit hours input
*/
@@ -108,8 +129,33 @@ export class HaBaseTimeInput extends LitElement {
protected render(): TemplateResult {
return html`
${this.label ? html`<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 +253,7 @@ export class HaBaseTimeInput extends LitElement {
<mwc-list-item value="PM">PM</mwc-list-item>
</ha-select>`}
</div>
${this.helper ? html`<div class="helper">${this.helper}</div>` : ""}
`;
}
@@ -303,6 +350,13 @@ export class HaBaseTimeInput extends LitElement {
color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));
padding-left: 4px;
}
.helper {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
font-size: 0.75rem;
padding-left: 16px;
padding-right: 16px;
}
`;
}

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

@@ -87,6 +87,8 @@ export class HaComboBox extends LitElement {
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean, reflect: true, attribute: "opened" })
private _opened?: boolean;
@@ -108,17 +110,22 @@ export class HaComboBox extends LitElement {
return this._comboBox.selectedItem;
}
public setInputValue(value: string) {
this._comboBox.value = value;
}
protected render(): TemplateResult {
return html`
<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 +136,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"

View File

@@ -35,6 +35,8 @@ export class HaDateInput extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property() public label?: string;
render() {
@@ -46,6 +48,7 @@ export class HaDateInput extends LitElement {
.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

@@ -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,4 +1,5 @@
import { HaFormSchema } from "./types";
import type { Selector } from "../../data/selector";
import type { HaFormSchema } from "./types";
export const computeInitialHaFormData = (
schema: HaFormSchema[]
@@ -31,6 +32,55 @@ export const computeInitialHaFormData = (
minutes: 0,
seconds: 0,
};
} else if ("selector" in field) {
const selector: Selector = field.selector;
if ("device" in selector) {
data[field.name] = selector.device.multiple ? [] : "";
} else if ("entity" in selector) {
data[field.name] = selector.entity.multiple ? [] : "";
} else if ("area" in selector) {
data[field.name] = selector.area.multiple ? [] : "";
} else if ("boolean" in selector) {
data[field.name] = false;
} else if (
"text" in selector ||
"addon" in selector ||
"attribute" in selector ||
"icon" in selector ||
"theme" in selector
) {
data[field.name] = "";
} else if ("number" in selector) {
data[field.name] = selector.number.min ?? 0;
} else if ("select" in selector) {
if (selector.select.options.length) {
data[field.name] = selector.select.options[0][0];
}
} else if ("duration" in selector) {
data[field.name] = {
hours: 0,
minutes: 0,
seconds: 0,
};
} else if ("time" in selector) {
data[field.name] = "00:00:00";
} else if ("date" in selector || "datetime" in selector) {
const now = new Date().toISOString().slice(0, 10);
data[field.name] = `${now} 00:00:00`;
} else if ("color_rgb" in selector) {
data[field.name] = [0, 0, 0];
} else if ("color_temp" in selector) {
data[field.name] = selector.color_temp.min_mireds ?? 153;
} else if (
"action" in selector ||
"media" in selector ||
"target" in selector
) {
data[field.name] = {};
} else {
throw new Error("Selector not supported in initial form data");
}
}
});
return data;

View File

@@ -1,16 +1,20 @@
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import "../ha-radio";
import type { HaRadio } from "../ha-radio";
import "../ha-select";
import type { HaSelect } from "../ha-select";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types";
import type { HomeAssistant } from "../../types";
import type {
HaFormElement,
HaFormSelectData,
HaFormSelectSchema,
} from "./types";
import type { SelectSelector } from "../../data/selector";
import "../ha-selector/ha-selector-select";
@customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public schema!: HaFormSelectSchema;
@property() public data!: HaFormSelectData;
@@ -19,60 +23,35 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-select", true) private _input?: HTMLElement;
public focus() {
if (this._input) {
this._input.focus();
}
}
private _selectSchema = memoizeOne(
(options): SelectSelector => ({
select: {
options: options.map((option) => ({
value: option[0],
label: option[1],
})),
},
})
);
protected render(): TemplateResult {
if (this.schema.required && this.schema.options!.length < 6) {
return html`
<div>
${this.label}
${this.schema.options.map(
([value, label]) => html`
<mwc-formfield .label=${label}>
<ha-radio
.checked=${value === this.data}
.value=${value}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-radio>
</mwc-formfield>
`
)}
</div>
`;
}
return html`
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
<ha-selector-select
.hass=${this.hass}
.schema=${this.schema}
.value=${this.data}
.label=${this.label}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
${!this.schema.required
? html`<mwc-list-item value=""></mwc-list-item>`
: ""}
${this.schema.options!.map(
([value, label]) => html`
<mwc-list-item .value=${value}>${label}</mwc-list-item>
`
)}
</ha-select>
.required=${this.schema.required}
.selector=${this._selectSchema(this.schema.options)}
@value-changed=${this._valueChanged}
></ha-selector-select>
`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
let value: string | undefined = (ev.target as HaSelect | HaRadio).value;
let value: string | undefined = ev.detail.value;
if (value === this.data) {
return;
@@ -86,15 +65,6 @@ export class HaFormSelect extends LitElement implements HaFormElement {
value,
});
}
static get styles(): CSSResultGroup {
return css`
ha-select,
mwc-formfield {
display: block;
}
`;
}
}
declare global {

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

@@ -106,6 +106,7 @@ export class HaForm extends LitElement implements HaFormElement {
.disabled=${this.disabled}
.helper=${this._computeHelper(item)}
.required=${item.required || false}
.context=${this._generateContext(item)}
></ha-selector>`
: dynamicElement(`ha-form-${item.type}`, {
schema: item,
@@ -115,6 +116,7 @@ export class HaForm extends LitElement implements HaFormElement {
hass: this.hass,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
context: this._generateContext(item),
})}
`;
})}
@@ -122,6 +124,20 @@ export class HaForm extends LitElement implements HaFormElement {
`;
}
private _generateContext(
schema: HaFormSchema
): Record<string, any> | undefined {
if (!schema.context) {
return undefined;
}
const context = {};
for (const [context_key, data_key] of Object.entries(schema.context)) {
context[context_key] = this.data[data_key];
}
return context;
}
protected createRenderRoot() {
const root = super.createRenderRoot();
// attach it as soon as possible to make sure we fetch all events.

View File

@@ -24,6 +24,7 @@ export interface HaFormBaseSchema {
// This value will be set initially when form is loaded
suggested_value?: HaFormData;
};
context?: Record<string, string>;
}
export interface HaFormGridSchema extends HaFormBaseSchema {

View File

@@ -39,6 +39,8 @@ export class HaIconPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public invalid = false;
@state() private _opened = false;
@@ -56,6 +58,7 @@ export class HaIconPicker extends LitElement {
.filteredItems=${iconItems}
.label=${this.label}
.disabled=${this.disabled}
.required=${this.required}
.placeholder=${this.placeholder}
.errorMessage=${this.errorMessage}
.invalid=${this.invalid}

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>
@@ -49,10 +49,15 @@ class HaLabeledSlider extends PolymerElement {
`;
}
_getTitle() {
return `${this.caption}${this.required ? "*" : ""}`;
}
static get properties() {
return {
caption: String,
disabled: Boolean,
required: Boolean,
min: Number,
max: Number,
pin: Boolean,

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

@@ -14,11 +14,17 @@ export class HaAddonSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`<ha-addon-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.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 {
@@ -21,6 +22,8 @@ export class HaAreaSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
@@ -28,27 +31,55 @@ export class HaAreaSelector extends LitElement {
oldSelector !== this.selector &&
this.selector.area.device?.integration
) {
this._loadConfigEntries();
getConfigEntries(this.hass, {
domain: this.selector.area.device.integration,
}).then((entries) => {
this._configEntries = entries;
});
}
}
}
protected render() {
return html`<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}
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}
.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 => {
@@ -85,12 +116,6 @@ export class HaAreaSelector extends LitElement {
}
return true;
};
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.area.device?.integration
);
}
}
declare global {

View File

@@ -1,9 +1,10 @@
import "../entity/ha-entity-attribute-picker";
import { html, LitElement } from "lit";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { AttributeSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entity-attribute-picker";
@customElement("ha-selector-attribute")
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@@ -17,18 +18,67 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@property() public context?: {
filter_entity?: string;
};
protected render() {
return html`
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${this.selector.attribute.entity_id}
.entityId=${this.selector.attribute.entity_id ||
this.context?.filter_entity}
.value=${this.value}
.label=${this.label}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-value
></ha-entity-attribute-picker>
`;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (
// No need to filter value if no value
!this.value ||
// Only adjust value if we used the context
this.selector.attribute.entity_id ||
// Only check if context has changed
!changedProps.has("context")
) {
return;
}
const oldContext = changedProps.get("context") as this["context"];
if (
!this.context ||
oldContext?.filter_entity === this.context.filter_entity
) {
return;
}
// Validate that that the attribute is still valid for this entity, else unselect.
let invalid = false;
if (this.context.filter_entity) {
const stateObj = this.hass.states[this.context.filter_entity];
if (!(stateObj && this.value in stateObj.attributes)) {
invalid = true;
}
} else {
invalid = this.value !== undefined;
}
if (invalid) {
fireEvent(this, "value-changed", {
value: undefined,
});
}
}
}
declare global {

View File

@@ -0,0 +1,62 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { hex2rgb, rgb2hex } from "../../common/color/convert-color";
import { fireEvent } from "../../common/dom/fire_event";
import type { ColorRGBSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-textfield";
@customElement("ha-selector-color_rgb")
export class HaColorRGBSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ColorRGBSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-textfield
type="color"
.value=${this.value ? rgb2hex(this.value as any) : ""}
.label=${this.label || ""}
.required=${this.required}
.disalbled=${this.disabled}
@change=${this._valueChanged}
></ha-textfield>
`;
}
private _valueChanged(ev: CustomEvent) {
const value = (ev.target as any).value;
fireEvent(this, "value-changed", {
value: hex2rgb(value),
});
}
static styles = css`
:host {
display: flex;
justify-content: flex-end;
align-items: center;
}
ha-textfield {
--text-field-padding: 8px;
min-width: 75px;
flex-grow: 1;
margin: 0 4px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-color_rgb": HaColorRGBSelector;
}
}

View File

@@ -0,0 +1,62 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { ColorTempSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-labeled-slider";
@customElement("ha-selector-color_temp")
export class HaColorTempSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: ColorTempSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-labeled-slider
pin
icon="hass:thermometer"
.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}
.required=${this.required}
@change=${this._valueChanged}
></ha-labeled-slider>
`;
}
private _valueChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", {
value: Number((ev.target as any).value),
});
}
static styles = css`
ha-labeled-slider {
--ha-slider-background: -webkit-linear-gradient(
right,
rgb(255, 160, 0) 0%,
white 50%,
rgb(166, 209, 255) 100%
);
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
--paper-slider-knob-start-border-color: var(--primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-color_temp": HaColorTempSelector;
}
}

View File

@@ -0,0 +1,39 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { DateSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-date-input";
@customElement("ha-selector-date")
export class HaDateSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: DateSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.value=${this.value}
.required=${this.required}
>
</ha-date-input>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-date": HaDateSelector;
}
}

View File

@@ -0,0 +1,78 @@
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { DateTimeSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-date-input";
import type { HaDateInput } from "../ha-date-input";
import "../ha-time-input";
import type { HaTimeInput } from "../ha-time-input";
@customElement("ha-selector-datetime")
export class HaDateTimeSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: DateTimeSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@query("ha-date-input") private _dateInput!: HaDateInput;
@query("ha-time-input") private _timeInput!: HaTimeInput;
protected render() {
const values = this.value?.split(" ");
return html`
<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>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: `${this._dateInput.value} ${this._timeInput.value}`,
});
}
static styles = css`
:host {
display: flex;
align-items: center;
flex-direction: row;
}
ha-date-input {
min-width: 150px;
margin-right: 4px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-datetime": HaDateTimeSelector;
}
}

View File

@@ -1,10 +1,11 @@
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { DeviceRegistryEntry } from "../../data/device_registry";
import { DeviceSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { DeviceSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../device/ha-device-picker";
import "../device/ha-devices-picker";
@customElement("ha-selector-device")
export class HaDeviceSelector extends LitElement {
@@ -20,30 +21,56 @@ export class HaDeviceSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (oldSelector !== this.selector && this.selector.device?.integration) {
this._loadConfigEntries();
getConfigEntries(this.hass, {
domain: this.selector.device.integration,
}).then((entries) => {
this._configEntries = entries;
});
}
}
}
protected render() {
return html`<ha-device-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.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}
allow-custom-entity
></ha-device-picker>`;
if (!this.selector.device.multiple) {
return html`
<ha-device-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.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}
.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>
`;
}
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
@@ -71,12 +98,6 @@ export class HaDeviceSelector extends LitElement {
}
return true;
};
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.device.integration
);
}
}
declare global {

View File

@@ -1,8 +1,8 @@
import "../ha-duration-input";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { DurationSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import type { DurationSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-duration-input";
@customElement("ha-selector-duration")
export class HaTimeDuration extends LitElement {
@@ -14,6 +14,8 @@ export class HaTimeDuration extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -22,9 +24,11 @@ export class HaTimeDuration extends LitElement {
return html`
<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,20 +1,23 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { subscribeEntityRegistry } from "../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import { EntitySelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entities-picker";
import "../entity/ha-entity-picker";
@customElement("ha-selector-entity")
export class HaEntitySelector extends SubscribeMixin(LitElement) {
export class HaEntitySelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: EntitySelector;
@state() private _entityPlaformLookup?: Record<string, string>;
@state() private _entitySources?: EntitySources;
@property() public value?: any;
@@ -22,60 +25,77 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
allow-custom-entity
></ha-entity-picker>`;
if (!this.selector.entity.multiple) {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.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>`;
}
return html`
${this.label ? html`<label>${this.label}</label>` : ""}
<ha-entities-picker
.hass=${this.hass}
.value=${this.value}
.entityFilter=${this._filterEntities}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.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 {
@@ -17,11 +17,15 @@ export class HaIconSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-icon-picker
.label=${this.label}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
.fallbackPath=${this.selector.icon.fallbackPath}
.placeholder=${this.selector.icon.placeholder}
@value-changed=${this._valueChanged}

View File

@@ -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";
@@ -52,7 +51,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,
},

View File

@@ -35,6 +35,8 @@ export class HaMediaSelector extends LitElement {
@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 +86,7 @@ export class HaMediaSelector extends LitElement {
.label=${this.label ||
this.hass.localize("ui.components.selectors.media.pick_media_player")}
.disabled=${this.disabled}
.required=${this.required}
include-domains='["media_player"]'
allow-custom-entity
@value-changed=${this._entityChanged}

View File

@@ -19,13 +19,15 @@ export class HaNumberSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public required = true;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`${this.selector.number.mode !== "box"
? html`${this.label}<ha-slider
? html`${this.label}${this.required ? "*" : ""}<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
@@ -46,8 +48,10 @@ export class HaNumberSelector extends LitElement {
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value || ""}
.value=${this.value ?? ""}
.step=${this.selector.number.step ?? 1}
helperPersistent
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${this.selector.number.unit_of_measurement}

View File

@@ -16,10 +16,13 @@ export class HaObjectSelector extends LitElement {
@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}
.readonly=${this.disabled}
.required=${this.required}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
@value-changed=${this._handleChange}

View File

@@ -1,19 +1,27 @@
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 { SelectOption, SelectSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import type { SelectOption, SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
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 {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public selector!: SelectSelector;
@property({ attribute: false }) public selector!: SelectSelector;
@property() public value?: string;
@property() public value?: string | string[];
@property() public label?: string;
@@ -21,43 +29,254 @@ export class HaSelectSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
@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;
@property({ type: Boolean }) public required = true;
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
})}
</ha-select>`;
@query("ha-combo-box", true) private comboBox!: HaComboBox;
private _filter = "";
protected render() {
const options = this.selector.select.options.map((option) =>
typeof option === "object" ? option : { value: option, label: option }
);
if (!this.selector.select.custom_value && this._mode === "list") {
if (!this.selector.select.multiple || this.required) {
return html`
<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>
`;
}
return html`
<div>
${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._checkboxChanged}
></ha-checkbox>
</ha-formfield>
`
)}
</div>
`;
}
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}
.disabled=${this.disabled}
.required=${this.required}
.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}
.disabled=${this.disabled}
.required=${this.required}
.items=${options}
.value=${this.value}
@filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged}
></ha-combo-box>
`;
}
return html`
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
${options.map(
(item: SelectOption) => html`
<mwc-list-item .value=${item.value}>${item.label}</mwc-list-item>
`
)}
</ha-select>
`;
}
private get _mode(): "list" | "dropdown" {
return (
this.selector.select.mode ||
(this.selector.select.options.length < 6 ? "list" : "dropdown")
);
}
private _valueChanged(ev) {
ev.stopPropagation();
if (this.disabled || !ev.target.value) {
const value = ev.detail?.value || ev.target.value;
if (this.disabled || !value) {
return;
}
fireEvent(this, "value-changed", {
value: ev.target.value,
value: value,
});
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
private _checkboxChanged(ev) {
ev.stopPropagation();
if (this.disabled) {
return;
}
let newValue: string[];
const value: string = ev.target.value;
const checked = ev.target.checked;
if (checked) {
if (!this.value) {
newValue = [value];
} else if (this.value.includes(value)) {
return;
} else {
newValue = [...this.value, value];
}
`;
} else {
if (!this.value?.includes(value)) {
return;
}
newValue = (this.value as string[]).filter((v) => v !== value);
}
fireEvent(this, "value-changed", {
value: newValue,
});
}
private async _removeItem(ev) {
const value: string[] = [...(this.value! as string[])];
value.splice(ev.target.idx, 1);
fireEvent(this, "value-changed", {
value,
});
await this.updateComplete;
this._filterChanged();
}
private _comboBoxValueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newValue = ev.detail.value;
if (this.disabled || newValue === "") {
return;
}
if (!this.selector.select.multiple) {
fireEvent(this, "value-changed", {
value: newValue,
});
return;
}
if (newValue !== undefined && this.value?.includes(newValue)) {
return;
}
setTimeout(() => {
this._filterChanged();
this.comboBox.setInputValue("");
}, 0);
const currentValue =
!this.value || this.value === "" ? [] : (this.value as string[]);
fireEvent(this, "value-changed", {
value: [...currentValue, newValue],
});
}
private _filterChanged(ev?: CustomEvent): void {
this._filter = ev?.detail.value || "";
const filteredItems = this.comboBox.items?.filter((item) => {
if (this.selector.select.multiple && this.value?.includes(item.value)) {
return false;
}
const label = item.label || item.value;
return label.toLowerCase().includes(this._filter?.toLowerCase());
});
if (this._filter && this.selector.select.custom_value) {
filteredItems?.unshift({ label: this._filter, value: this._filter });
}
this.comboBox.filteredItems = filteredItems;
}
static styles = css`
ha-select,
mwc-formfield,
ha-formfield {
display: block;
}
`;
}
declare global {

View File

@@ -134,9 +134,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) =>
entry.domain ===
(this.selector.target.device?.integration ||
this.selector.target.entity?.integration)
entry.domain === this.selector.target.device?.integration ||
entry.domain === this.selector.target.entity?.integration
);
}

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

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

View File

@@ -8,6 +8,9 @@ import "./ha-selector-addon";
import "./ha-selector-area";
import "./ha-selector-attribute";
import "./ha-selector-boolean";
import "./ha-selector-color-rgb";
import "./ha-selector-date";
import "./ha-selector-datetime";
import "./ha-selector-device";
import "./ha-selector-duration";
import "./ha-selector-entity";
@@ -21,6 +24,7 @@ import "./ha-selector-icon";
import "./ha-selector-media";
import "./ha-selector-theme";
import "./ha-selector-location";
import "./ha-selector-color-temp";
@customElement("ha-selector")
export class HaSelector extends LitElement {
@@ -40,6 +44,8 @@ export class HaSelector extends LitElement {
@property({ type: Boolean }) public required = true;
@property() public context?: Record<string, any>;
public focus() {
this.shadowRoot?.getElementById("selector")?.focus();
}
@@ -59,6 +65,7 @@ export class HaSelector extends LitElement {
disabled: this.disabled,
required: this.required,
helper: this.helper,
context: this.context,
id: "selector",
})}
`;

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

@@ -37,6 +37,7 @@ import { LocalStorage } from "../common/decorators/local-storage";
import { fireEvent } from "../common/dom/fire_event";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { stringCompare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl";
import { ActionHandlerDetail } from "../data/lovelace";
@@ -44,6 +45,7 @@ import {
PersistentNotification,
subscribeNotifications,
} from "../data/persistent_notification";
import { updateCanInstall, UpdateEntity } from "../data/update";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -68,7 +70,6 @@ const SORT_VALUE_URL_PATHS = {
const PANEL_ICONS = {
calendar: mdiCalendar,
config: mdiCog,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
history: mdiChartBox,
@@ -190,6 +191,8 @@ class HaSidebar extends LitElement {
@state() private _notifications?: PersistentNotification[];
@state() private _updatesCount = 0;
@state() private _renderEmptySortable = false;
private _mouseLeaveTimeout?: number;
@@ -235,6 +238,7 @@ class HaSidebar extends LitElement {
changedProps.has("narrow") ||
changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_updatesCount") ||
changedProps.has("_notifications") ||
changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") ||
@@ -290,6 +294,12 @@ class HaSidebar extends LitElement {
toggleAttribute(this, "rtl", computeRTL(this.hass));
}
this._updatesCount = Object.values(this.hass.states).filter(
(entity) =>
computeStateDomain(entity) === "update" &&
updateCanInstall(entity as UpdateEntity)
).length;
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
@@ -387,35 +397,37 @@ class HaSidebar extends LitElement {
icon?: string | null,
iconPath?: string | null
) {
return html`
<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,6 +489,35 @@ 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
? this._notifications.length
@@ -953,18 +994,21 @@ class HaSidebar extends LitElement {
height: 1px;
background-color: var(--divider-color);
}
.notifications-container {
.notifications-container,
.configuration-container {
display: flex;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) .notifications-container {
:host([rtl]) .notifications-container,
:host([rtl]) .configuration-container {
margin-left: initial;
margin-right: env(safe-area-inset-right);
}
.notifications {
cursor: pointer;
}
.notifications .item-text {
.notifications .item-text,
.configuration .item-text {
flex: 1;
}
.profile {
@@ -988,7 +1032,8 @@ class HaSidebar extends LitElement {
margin-right: 8px;
}
.notification-badge {
.notification-badge,
.configuration-badge {
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;
@@ -999,7 +1044,11 @@ class HaSidebar extends LitElement {
padding: 0px 6px;
color: var(--text-accent-color, var(--text-primary-color));
}
ha-svg-icon + .notification-badge {
.configuration-badge {
background-color: var(--primary-color);
}
ha-svg-icon + .notification-badge,
ha-svg-icon + .configuration-badge {
position: absolute;
bottom: 14px;
left: 26px;

View File

@@ -42,9 +42,7 @@ export class HaTab extends LitElement {
@keydown=${this._handleKeyDown}
>
${this.narrow ? html`<slot name="icon"></slot>` : ""}
${!this.narrow || this.active
? html`<span class="name">${this.name}</span>`
: ""}
<span class="name">${this.name}</span>
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
</div>
`;

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

@@ -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")
@@ -16,6 +16,8 @@ export class HaTimeInput extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "enable-second" })
public enableSecond = false;
@@ -43,6 +45,7 @@ export class HaTimeInput extends LitElement {
.disabled=${this.disabled}
@value-changed=${this._timeChanged}
.enableSecond=${this.enableSecond}
.required=${this.required}
></ha-base-time-input>
`;
}

View File

@@ -31,6 +31,10 @@ export class HaYamlEditor extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public readOnly = false;
@property({ type: Boolean }) public required = false;
@state() private _yaml = "";
public setValue(value): void {
@@ -57,10 +61,11 @@ export class HaYamlEditor extends LitElement {
return html``;
}
return html`
${this.label ? html`<p>${this.label}</p>` : ""}
${this.label ? html`<p>${this.label}${this.required ? "*" : ""}</p>` : ""}
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}
.readOnly=${this.readOnly}
mode="yaml"
autocomplete-entities
.error=${this.isValid === false}

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;
}
`;
}
}

View File

@@ -62,6 +62,7 @@ export interface ContextConstraint {
export interface BaseTrigger {
platform: string;
id?: string;
variables?: Record<string, unknown>;
}
export interface StateTrigger extends BaseTrigger {

36
src/data/backup.ts Normal file
View File

@@ -0,0 +1,36 @@
import { HomeAssistant } from "../types";
export interface BackupContent {
slug: string;
date: string;
name: string;
size: number;
path: string;
}
export interface BackupData {
backing_up: boolean;
backups: BackupContent[];
}
export const getBackupDownloadUrl = (slug: string) =>
`/api/backup/download/${slug}`;
export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupData> =>
hass.callWS({
type: "backup/info",
});
export const removeBackup = (
hass: HomeAssistant,
slug: string
): Promise<void> =>
hass.callWS({
type: "backup/remove",
slug,
});
export const generateBackup = (hass: HomeAssistant): Promise<BackupContent> =>
hass.callWS({
type: "backup/generate",
});

View File

@@ -7,6 +7,7 @@ import {
HistoryResult,
LineChartUnit,
TimelineEntity,
entityIdHistoryNeedsAttributes,
} from "./history";
export interface CacheConfig {
@@ -53,7 +54,17 @@ export const getRecent = (
return cache.data;
}
const prom = fetchRecent(hass, entityId, startTime, endTime).then(
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const prom = fetchRecent(
hass,
entityId,
startTime,
endTime,
false,
undefined,
true,
noAttributes
).then(
(stateHistory) => computeHistory(hass, stateHistory, localize),
(err) => {
delete RECENT_CACHE[entityId];
@@ -120,6 +131,7 @@ export const getRecentWithCache = (
}
const curCacheProm = cache.prom;
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const genProm = async () => {
let fetchedHistory: HassEntity[][];
@@ -132,7 +144,10 @@ export const getRecentWithCache = (
entityId,
toFetchStartTime,
endTime,
appendingToCache
appendingToCache,
undefined,
true,
noAttributes
),
]);
fetchedHistory = results[1];

View File

@@ -2,7 +2,7 @@ import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
import { timeCacheEntityPromiseFunc } from "../common/util/time-cache-entity-promise-func";
import { HomeAssistant } from "../types";
import { getSignedPath } from "./auth";
@@ -50,7 +50,7 @@ export const fetchThumbnailUrlWithCache = async (
width: number,
height: number
) => {
const base_url = await timeCachePromiseFunc(
const base_url = await timeCacheEntityPromiseFunc(
"_cameraTmbUrl",
9000,
fetchThumbnailUrl,

View File

@@ -6,6 +6,7 @@ import { AutomationConfig } from "./automation";
interface CloudStatusNotLoggedIn {
logged_in: false;
cloud: "disconnected" | "connecting" | "connected";
http_use_ssl: boolean;
}
export interface GoogleEntityConfig {
@@ -59,6 +60,7 @@ export interface CloudStatusLoggedIn {
remote_connected: boolean;
remote_certificate: undefined | CertificateInformation;
http_use_ssl: boolean;
active_subscription: boolean;
}
export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn;

View File

@@ -34,8 +34,24 @@ export const ERROR_STATES: ConfigEntry["state"][] = [
"setup_retry",
];
export const getConfigEntries = (hass: HomeAssistant) =>
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
export const getConfigEntries = (
hass: HomeAssistant,
filters?: { type?: "helper" | "integration"; domain?: string }
): Promise<ConfigEntry[]> => {
const params = new URLSearchParams();
if (filters) {
if (filters.type) {
params.append("type", filters.type);
}
if (filters.domain) {
params.append("domain", filters.domain);
}
}
return hass.callApi<ConfigEntry[]>(
"GET",
`config/config_entries/entry?${params.toString()}`
);
};
export const updateConfigEntry = (
hass: HomeAssistant,

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