Compare commits

...

119 Commits

Author SHA1 Message Date
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
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
e9003ac35e Merge pull request #11969 from home-assistant/patch-release 2022-03-07 17:08:58 +01:00
Bram Kragten
1dd5214b42 Bumped version to 20220301.1 2022-03-07 16:51:41 +01:00
Bram Kragten
96738350bb Add location selector, convert zone editor (#11902) 2022-03-07 16:51:20 +01:00
Bram Kragten
5bdecf57cf Convert file upload to mdc (#11906) 2022-03-07 16:51:02 +01:00
Bram Kragten
ec12282f8c A11y expansion panel (#11967) 2022-03-07 16:50:42 +01:00
Zack Barett
552dbca201 Fix for Statistics Editor (#11942)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-03-07 16:50:29 +01:00
Bram Kragten
0bbc0ebb3c Make min width of select configurable (#11965) 2022-03-07 16:50:14 +01:00
Bram Kragten
ac7acc5802 Fix humidifier more info mode dropdown (#11964) 2022-03-07 16:49:59 +01:00
Philip Allgaier
64e1d160d1 Correct media upload error + add file name (#11949)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-03-07 16:49:40 +01:00
Bram Kragten
8e51878b6d Convert inputs (#11907)
* Convert inputs

* Update dialog-thingtalk.ts

* imports
2022-03-07 16:49:20 +01:00
Paulus Schoutsen
7c94ced303 Guard setting up config flow for an unsupported domain (#11937) 2022-03-07 16:48:57 +01:00
Bram Kragten
a040e1d5e0 Convert lovelace config dialogs to ha-form (#11910) 2022-03-07 16:48:36 +01:00
Bram Kragten
87c7407857 Convert objects to string in config flow error (#11908) 2022-03-07 16:48:16 +01:00
Bram Kragten
d0d0c44ec7 Fix quickbar overlaying, fix click handling (#11900) 2022-03-07 16:47:56 +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
Paulus Schoutsen
7f086c0900 Merge pull request #11899 from home-assistant/dev 2022-03-01 15:06:56 -08:00
Paulus Schoutsen
8882624618 Merge pull request #11867 from home-assistant/dev 2022-02-26 13:41:41 -08:00
Paulus Schoutsen
ffac3d055e Merge pull request #11848 from home-assistant/dev 2022-02-24 16:37:06 -08:00
Paulus Schoutsen
09f8f816d1 Merge pull request #11809 from home-assistant/dev (#11809)
* Fix config card rtl issues

* Remove optional field from ha-form schema type (#11538)

* Add entity id autocompletion to YAML code editors (#11099)

* Add selectors to ha-form (#11534)

* Allow translate gas total (#11547)

* Migrate combobox to mwc (#11546)

* New date picker (#11555)

* Link via device on device page (#11554)

Co-authored-by: Zack Barett <arnett.zackary@gmail.com>

* Add integration_discovery to discovery sources (#11564)

* Remember filter between navigation (#11565)

* Convert selectors to MWC (#11543)

* Covert area picker to combo-box (#11562)

* Convert entity picker to ha-combo (#11560)

* Convert entity picker to ha-combo

* Update ha-entity-picker.ts

* Handle empty better

* Clear value when no device/area/entity

* Update links on info page (#11590)

* Migrate (input) select entities to mwc (#11591)

* Convert HaFormSchemas to use selectors (#11589)

* Fix number selector (#11585)

* Convert entity-attribute picker to ha-combo-box (#11587)

* Convert icon picker to ha-combobox (#11586)

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

* Convert area-devices picker (#11588)

* Convert device automation picker to mwc (#11592)

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

* Fix clearing device in device action (#11594)

* dark mode fixes (#11595)

* Only show stable add-ons in the store if not advanced mode (#11596)

* Convert Automation Action Choose to HA Form (#11597)

* Convert Auatomation Action Choose to HA Form

* remove log

* Remove Import

* Replace checkboxes in list items with `check-list-item` (#11610)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Make textarea grow on input (#11618)

* Update lit-virtualizer (#11623)

* Convert time inputs to Lit + mwc (#11609)

* Set initial focus for device, area, and entity dialogs (#11622)

* Add aria-label to table headers with no title (#11503)

* Add loadCardHelpers to cast scope (#11616)

* Update code editor to material 3 look (#11628)

* Set button role on button card and handle enter and space (#11627)

* Only load ha-selector when needed (#11630)

* Fix service control for older browsers (#11629)

* Migrate a bunch of paper-dropdowns (#11626)

* Merged too fast for Bram :) Code improv (#11632)

* Add support for opening camera media source (#11633)

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

* Create error when trying to backup wile system in freeze (#11634)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Add missing type to create device automation/script heading (#11635)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Generate random webhook_id and add copy button (#11568)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Migrate search bar to mwc (#11637)

* fix data-table row handlers (#11638)

* Bunch of fixes and cleanup (#11636)

* State Trigger -> HA Form (#11631)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Allow uploading media (#11615)

* Allow uploading media

* Update path

* Use current item we already have

* Update src/panels/media-browser/ha-panel-media-browser.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Use alert dialog and use button for add media

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Add Attribute Picker as a selector - add to state trigger (#11641)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Time Pattern to HA Form (#11648)

* MQTT Trigger to Ha-Form (#11643)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Convert Sun to Ha Form (#11647)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Geo Location Trigger to HA - Form (#11644)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* HA Trigger to HA Form (#11645)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Make HA Form set required to false for selectors (#11649)

* Fix Lovelace Empty Menu when not advanced or admin (#11660)

* Add support for media player assumed state (#11642)

* Improve search and filters on mobile + fix close button in search field (#11662)

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

* Allow adding Zigbee/Zwave device (#11650)

* Numerical State to HA-Form (#11646)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Filter fixes (#11664)

* Add WORKSPACE_DIRECTORY environment variable to devcontainer and script.core (#11477)

Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>

* hotfix history view on missing state (#11663)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Improve robustness of hls media player (#11672)

* Revert compute state display show empty string as unknown (#11677)

* Set initial focus for some more dialogs (#11676)

* Limit types of media that can be uploaded to local media (#11683)

* Don't show toggle always on more info (#11640)

* Add TTS to media browser (#11679)

* Omit Device info and actions for connected controller nodes (#11673)

* Script Editor to Ha Form (#11601)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Another round of paper-dropdown -> mwc-select conversion (#11674)

* Another round of paper-dropdown -> mwc-select conversion

* ha-pick-language-row -> Lit

* Update hui-view-editor.ts

* Cleanup imports

* hassio

* Add explicit imports

* hassio fixes (#11688)

* Dont exclude domain for area and device (#11689)

* Try to keep the browsing stack when changing players in media panel (#11681)

* Allow uploading multiple files (#11687)

* Bumped version to 20220214.0

* Group helpers not in an area in a single card (#11690)

* Improve `stripPrefixFromEntityName` to handle colon and space separator (#11691)

* Display transmitted messages in MQTT debug info dialog (#11531)

* Latest paper-dropdown -> mwc-select conversion (#11692)

* This adds back mobile click accessibility (#11693)

* Updated text part 2 (#11686)

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

* Set initial focus for lovelace dialogs (#11667)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Migrate all lovelace elements to mwc (#11695)

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

* Fix import

* Clean up some imports (#11696)

* Convert triple dots to single char in translations (#11697)

* Fixes remote icon state color (#11698)

* Convert scene action to service call (#11705)

* Convert scene action to service call

* fix describeAction

* rename to metadata

* Update script.ts

* Fix mode selection in automation editor (#11707)

* Remove duplicate gallery page (#11711)

* Add bottom padding to config links list with safe-area-inset-bottom (#11704)

* Bump hls.js to v1.1.5 (#11712)

* Make zwave_js config panel inclusion state aware (#11556)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Fix mwc-select in lovelace editors (#11708)

* Add signed add-on capability and adjust max rating (#11703)

* Add support for removing config entry from a device

* Tweak

* Fix lint error

* Tweak

* Prettier

* Add play media action (#11702)

Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Debounce refresh the cloud status if Google events happen (#11721)

* Remove custom MQTT delete device button (#11724)

* Apply suggestions from code review

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Correct typing

* Prettier

* Remove useless Array.isArray check

* Remove custom Tasmota delete device button (#11725)

* Automation Conditions to conversion to ha-form or mwc (#11727)

* Set initial focus for energy dialogs (#11730)

* Entity Settings Page to MWC 3 (#11694)

* Show why relayer is reconnecting (#11732)

* Change words for trigger condition (#11733)

* Update media player more info (#11734)

* Pass hass to ha-form to enable selectors (#11739)

* Bumped version to 20220220.0

* Add link to the selector docs

* TTS form no longer showed due to import oopsie (#11742)

* Improve logo rendering for playing media in browser (#11741)

* Fix media upload on iOS (#11740)

* Handle inifinity media duration (#11749)

* Show when media is being loaded (#11750)

* Lovelace Entity Card Editor to Ha Form - Adds Theme Selector and HaFormColumn (#11731)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Set initial focus for supervisor dialogs (#11710)

* Convert Automation Actions to mwc/ha-form + other automation items (#11753)

* Selector: remove text value when not required and empty (#11754)

* Convert date-range-picker to mwc (#11755)

* Radio Browser is now added during onboarding (#11756)

* Add support for the media browser My link (#11757)

* Show Home Assistant when creating partial backup (#11758)

* Fix zwave migration (#11751)

* Allow config entries to be reloaded when they are in setup_retry state (#11759)

* Area Card Editor to Ha Form (#11762)

* Fix WebRTC player stream playback when disconnected/connected (#11764)

* set theme to undefined when no theme (#11765)

* Paper input migrations (#11766)

* Only show description when set (#11772)

* Thermostat Editor to HA - Form (#11763)

* Thermostat - Ha Form

* Update hui-thermostat-card-editor.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Alarm Card Editor to HA Form (#11760)

* Move to ha-form

* Update hui-alarm-panel-card-editor.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Change icons for cover with device_class curtain (#11752)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* no need for memoize

* Include scoped custom element polyfill (#11776)

* Show triggered in automation editor (#11771)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Allow changing volume media player entity (#11781)

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

* Add community section (#11779)

* Bumped version to 20220222.0

* Fix State Condition 'For' Data (#11782)

* entities card editor to MWC (#11785)

* Fix ripple corner radius for button card (#11780)

* Condition Card Editor to MWC (#11783)

* Show number of hidden items (#11786)

* Put volume slider in the middle of the button (#11788)

* Add media management dialog (#11787)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Convert alarm control panel more info (#11791)

* Convert alarm control panel more info

* Update more-info-alarm_control_panel.ts

* Update src/dialogs/more-info/controls/more-info-alarm_control_panel.ts

* Apply suggestions from code review

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

* import

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

* Migrate more-info configurator (#11792)

* Migrate more-info configurator

* Update more-info-configurator.ts

* Update src/dialogs/more-info/controls/more-info-configurator.ts

* Update src/dialogs/more-info/controls/more-info-configurator.ts

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

* Import

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

* Convert more info lock (#11794)

* Add Margin to Tip (#11790)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Dont render double label on number selector (#11796)

* Input conversion in dev tools (#11795)

* Gauge Editor to Ha Form (#11793)

* Stop spinning when opening media in dialog (#11800)

* Fix Entities picker (#11802)

* Migrate single textfields (#11799)

* Migrate single textfields

* Update ha-config-name-form.ts

* Update dialog-area-registry-detail.ts

* Update manual-automation-editor.ts

* Update manual-automation-editor.ts

* required to number selector fix script

* review

* change repository url and project description (#11801)

* Calendar card to HA Form (#11784)

* Graph Footer to MWC (#11803)

* History Graph Editor to ha form (#11797)

* Glance editor to ha-form (#11804)

* Grid Card to HA Form (#11798)

* Button editor to ha-form (#11808)

* Bumped version to 20220223.0

* mwc-select -> ha-select (#11806)

Co-authored-by: Yosi Levy <yosilevy@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Yosi Levy <37745463+yosilevy@users.noreply.github.com>
Co-authored-by: Kuba Wolanin <hi@kubawolanin.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Zack <zackbarett@hey.com>
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
Co-authored-by: Patrick ZAJDA <patrick@zajda.fr>
Co-authored-by: Thomas Lovén <thomasloven@gmail.com>
Co-authored-by: Eric Severance <esev@esev.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
Co-authored-by: lintaba <lintaba@gmail.com>
Co-authored-by: Allen Porter <allen@thebends.org>
Co-authored-by: kpine <keith.pine@gmail.com>
Co-authored-by: Brandon Rothweiler <brandonrothweiler@gmail.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Matthias de Baat <matthias.debaat@nabucasa.com>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Josh McCarty <josh@joshmccarty.com>
Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com>
Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com>
Co-authored-by: Pascal Winters <pascal@famwinters.com>
Co-authored-by: Robin Wittebol <robinwittebol@live.nl>
Co-authored-by: Tomasz <t.jagusz@gmail.com>
2022-02-23 11:11:41 -08: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
176 changed files with 4468 additions and 7483 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";

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,6 +36,10 @@ module.exports = [
category: "misc",
header: "Miscelaneous",
},
{
category: "brand",
header: "Brand",
},
{
category: "user-test",
header: "User Tests",

View File

@@ -78,6 +78,9 @@ class DemoCards extends LitElement {
ha-formfield {
margin-right: 16px;
}
#container {
background-color: var(--primary-background-color);
}
`;
}

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;

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";
@@ -53,10 +54,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 +92,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 +193,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 +233,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

@@ -12,6 +12,98 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
import { getEntity } from "../../../../src/fake_data/entity";
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;
@@ -38,6 +130,8 @@ const SCHEMAS: {
select: "Select",
icon: "Icon",
media: "Media",
location: "Location",
entities: "Entities",
},
schema: [
{ name: "addon", selector: { addon: {} } },
@@ -45,6 +139,7 @@ const SCHEMAS: {
{
name: "Attribute",
selector: { attribute: { entity_id: "" } },
context: { filter_entity: "entity" },
},
{ name: "Device", selector: { device: {} } },
{ name: "Duration", selector: { duration: {} } },
@@ -75,6 +170,14 @@ const SCHEMAS: {
media: {},
},
},
{
name: "location",
selector: { location: { radius: true, icon: "mdi:home" } },
},
{
name: "entities",
selector: { entity: { multiple: true } },
},
],
},
{
@@ -315,9 +418,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

@@ -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,12 +164,44 @@ const SCHEMAS: {
},
},
object: { name: "Object", selector: { object: {} } },
select_radio: {
name: "Select (Radio)",
selector: { select: { options: ["Option 1", "Option 2"] } },
},
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",
],
},
},
},
icon: { name: "Icon", selector: { icon: {} } },
media: { name: "Media", selector: { media: {} } },
location: { name: "Location", selector: { location: {} } },
location_radius: {
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 } } },
},
},
];

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

@@ -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,140 @@
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,
} 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";
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",
}),
];
@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);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-update": DemoMoreInfoUpdate;
}
}

View File

@@ -1,5 +1,4 @@
import { mdiFolderUpload } from "@mdi/js";
import "@polymer/paper-input/paper-input-container";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event";

View File

@@ -121,7 +121,8 @@ export class HassioMain extends SupervisorBaseElement {
this.parentElement,
this.hass.themes,
themeName,
themeSettings
themeSettings,
true
);
}
}

View File

@@ -79,7 +79,6 @@
"@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.0
version = 20220322.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -101,13 +101,19 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this._fetchAuthProviders();
if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement(document.documentElement, {
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
});
applyThemesOnElement(
document.documentElement,
{
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
if (!this.redirectUri) {

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

@@ -31,11 +31,12 @@ export const applyThemesOnElement = (
element,
themes: HomeAssistant["themes"],
selectedTheme?: string,
themeSettings?: Partial<HomeAssistant["selectedTheme"]>
themeSettings?: Partial<HomeAssistant["selectedTheme"]>,
main?: boolean
) => {
// If there is no explicitly desired theme provided, we automatically
// If there is no explicitly desired theme provided, and the element is the main element we automatically
// use the active one from `themes`.
const themeToApply = selectedTheme || themes.theme;
const themeToApply = selectedTheme || (main ? themes.theme : undefined);
// If there is no explicitly desired dark mode provided, we automatically
// use the active one from `themes`.
@@ -47,7 +48,7 @@ export const applyThemesOnElement = (
let cacheKey = themeToApply;
let themeRules: Partial<ThemeVars> = {};
if (darkMode) {
if (themeToApply && darkMode) {
cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles };
}

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

@@ -70,9 +70,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

@@ -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

@@ -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";
@@ -116,6 +116,12 @@ class HaDevicesPicker extends LitElement {
this._updateDevices([...currentDevices, toAdd]);
}
static override styles = css`
div {
margin-top: 8px;
}
`;
}
declare global {

View File

@@ -46,11 +46,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 +85,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,6 +102,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}
@@ -94,7 +116,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

@@ -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;
@@ -74,6 +78,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 +116,7 @@ export class HaEntityPicker extends LitElement {
private _initedStates = false;
private _states: HassEntity[] = [];
private _states: HassEntityWithCachedName[] = [];
private _getStates = memoizeOne(
(
@@ -106,9 +126,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 +144,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 +158,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 +194,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 +243,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 +281,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;

View File

@@ -41,7 +41,7 @@ export class HaDateInput extends LitElement {
return html`<ha-textfield
.label=${this.label}
.disabled=${this.disabled}
iconTrailing="calendar"
iconTrailing
@click=${this._openDialog}
.value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)

View File

@@ -1,6 +1,13 @@
import { mdiChevronDown } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
@@ -16,11 +23,21 @@ class HaExpansionPanel extends LitElement {
@property() secondary?: string;
@state() _showContent = this.expanded;
@query(".container") private _container!: HTMLDivElement;
protected render(): TemplateResult {
return html`
<div class="summary" @click=${this._toggleContainer}>
<div
id="summary"
@click=${this._toggleContainer}
@keydown=${this._toggleContainer}
role="button"
tabindex="0"
aria-expanded=${this.expanded}
aria-controls="sect1"
>
<slot class="header" name="header">
${this.header}
<slot class="secondary" name="secondary">${this.secondary}</slot>
@@ -33,21 +50,37 @@ class HaExpansionPanel extends LitElement {
<div
class="container ${classMap({ expanded: this.expanded })}"
@transitionend=${this._handleTransitionEnd}
role="region"
aria-labelledby="summary"
aria-hidden=${!this.expanded}
tabindex="-1"
>
<slot></slot>
${this._showContent ? html`<slot></slot>` : ""}
</div>
`;
}
private _handleTransitionEnd() {
this._container.style.removeProperty("height");
protected willUpdate(changedProps: PropertyValues) {
if (changedProps.has("expanded") && this.expanded) {
this._showContent = this.expanded;
}
}
private async _toggleContainer(): Promise<void> {
private _handleTransitionEnd() {
this._container.style.removeProperty("height");
this._showContent = this.expanded;
}
private async _toggleContainer(ev): Promise<void> {
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
return;
}
ev.preventDefault();
const newExpanded = !this.expanded;
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
if (newExpanded) {
this._showContent = true;
// allow for dynamic content to be rendered
await nextRender();
}
@@ -80,17 +113,21 @@ class HaExpansionPanel extends LitElement {
var(--divider-color, #e0e0e0)
);
border-radius: var(--ha-card-border-radius, 4px);
padding: 0 8px;
}
.summary {
#summary {
display: flex;
padding: var(--expansion-panel-summary-padding, 0);
padding: var(--expansion-panel-summary-padding, 0 8px);
min-height: 48px;
align-items: center;
cursor: pointer;
overflow: hidden;
font-weight: 500;
outline: none;
}
#summary:focus {
background: var(--input-fill-color);
}
.summary-icon {
@@ -103,6 +140,7 @@ class HaExpansionPanel extends LitElement {
}
.container {
padding: var(--expansion-panel-content-padding, 0 8px);
overflow: hidden;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
height: 0px;

View File

@@ -1,6 +1,5 @@
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
import { mdiClose } from "@mdi/js";
import "@polymer/iron-input/iron-input";
import "@polymer/paper-input/paper-input-container";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -21,7 +20,7 @@ export class HaFileUpload extends LitElement {
@property() public accept!: string;
@property() public icon!: string;
@property() public icon?: string;
@property() public label!: string;
@@ -39,15 +38,7 @@ export class HaFileUpload extends LitElement {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
if (this.autoOpenFileDialog) {
this._input?.click();
}
}
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("_drag") && !this.uploading) {
(
this.shadowRoot!.querySelector("paper-input-container") as any
)._setFocused(this._drag);
this._openFilePicker();
}
}
@@ -60,51 +51,75 @@ export class HaFileUpload extends LitElement {
active
></ha-circular-progress>`
: html`
<label for="input">
<paper-input-container
.alwaysFloatLabel=${Boolean(this.value)}
@drop=${this._handleDrop}
@dragenter=${this._handleDragStart}
@dragover=${this._handleDragStart}
@dragleave=${this._handleDragEnd}
@dragend=${this._handleDragEnd}
class=${classMap({
dragged: this._drag,
})}
<label
for="input"
class="mdc-text-field mdc-text-field--filled ${classMap({
"mdc-text-field--focused": this._drag,
"mdc-text-field--with-leading-icon": Boolean(this.icon),
"mdc-text-field--with-trailing-icon": Boolean(this.value),
})}"
@drop=${this._handleDrop}
@dragenter=${this._handleDragStart}
@dragover=${this._handleDragStart}
@dragleave=${this._handleDragEnd}
@dragend=${this._handleDragEnd}
>
<span class="mdc-text-field__ripple"></span>
<span
class="mdc-floating-label ${this.value || this._drag
? "mdc-floating-label--float-above"
: ""}"
id="label"
>${this.label}</span
>
<label for="input" slot="label"> ${this.label} </label>
<iron-input slot="input">
<input
id="input"
type="file"
class="file"
accept=${this.accept}
@change=${this._handleFilePicked}
/>
${this.value}
</iron-input>
${this.value
? html`
<ha-icon-button
slot="suffix"
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.close") ||
"close"}
.path=${mdiClose}
></ha-icon-button>
`
: html`
<ha-icon-button
slot="suffix"
.path=${this.icon}
></ha-icon-button>
`}
</paper-input-container>
${this.icon
? html`<span
class="mdc-text-field__icon mdc-text-field__icon--leading"
tabindex="-1"
>
<ha-icon-button
@click=${this._openFilePicker}
.path=${this.icon}
></ha-icon-button>
</span>`
: ""}
<div class="value">${this.value}</div>
<input
id="input"
type="file"
class="mdc-text-field__input file"
accept=${this.accept}
@change=${this._handleFilePicked}
aria-labelledby="label"
/>
${this.value
? html`<span
class="mdc-text-field__icon mdc-text-field__icon--trailing"
tabindex="1"
>
<ha-icon-button
slot="suffix"
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.close") ||
"close"}
.path=${mdiClose}
></ha-icon-button>
</span>`
: ""}
<span
class="mdc-line-ripple ${this._drag
? "mdc-line-ripple--active"
: ""}"
></span>
</label>
`}
`;
}
private _openFilePicker() {
this._input?.click();
}
private _handleDrop(ev: DragEvent) {
ev.preventDefault();
ev.stopPropagation();
@@ -137,40 +152,66 @@ export class HaFileUpload extends LitElement {
}
static get styles() {
return css`
paper-input-container {
position: relative;
padding: 8px;
margin: 0 -8px;
}
paper-input-container.dragged:before {
position: var(--layout-fit_-_position);
top: var(--layout-fit_-_top);
right: var(--layout-fit_-_right);
bottom: var(--layout-fit_-_bottom);
left: var(--layout-fit_-_left);
background: currentColor;
content: "";
opacity: var(--dark-divider-opacity);
pointer-events: none;
border-radius: 4px;
}
input.file {
display: none;
}
img {
max-width: 125px;
max-height: 125px;
}
ha-icon-button {
--mdc-icon-button-size: 24px;
--mdc-icon-size: 20px;
}
ha-circular-progress {
display: block;
text-align-last: center;
}
`;
return [
styles,
css`
:host {
display: block;
}
.mdc-text-field--filled {
height: auto;
padding-top: 16px;
cursor: pointer;
}
.mdc-text-field--filled.mdc-text-field--with-trailing-icon {
padding-top: 28px;
}
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
color: var(--secondary-text-color);
}
.mdc-text-field--filled.mdc-text-field--with-trailing-icon
.mdc-text-field__icon {
align-self: flex-end;
}
.mdc-text-field__icon--leading {
margin-bottom: 12px;
}
.mdc-text-field--filled .mdc-floating-label--float-above {
transform: scale(0.75);
top: 8px;
}
.dragged:before {
position: var(--layout-fit_-_position);
top: var(--layout-fit_-_top);
right: var(--layout-fit_-_right);
bottom: var(--layout-fit_-_bottom);
left: var(--layout-fit_-_left);
background: currentColor;
content: "";
opacity: var(--dark-divider-opacity);
pointer-events: none;
border-radius: 4px;
}
.value {
width: 100%;
}
input.file {
display: none;
}
img {
max-width: 100%;
max-height: 125px;
}
ha-icon-button {
--mdc-icon-button-size: 24px;
--mdc-icon-size: 20px;
}
ha-circular-progress {
display: block;
text-align-last: center;
}
`,
];
}
}

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,25 @@ export const computeInitialHaFormData = (
minutes: 0,
seconds: 0,
};
} else if ("selector" in field) {
const selector: Selector = field.selector;
if ("boolean" in selector) {
data[field.name] = false;
} else if ("text" in selector) {
data[field.name] = "";
} else if ("number" in selector) {
data[field.name] = "min" in selector.number ? 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,
};
}
}
});
return data;

View File

@@ -9,7 +9,9 @@ export class HaFormConstant extends LitElement implements HaFormElement {
@property() public label!: string;
protected render(): TemplateResult {
return html`<span class="label">${this.label}</span>: ${this.schema.value}`;
return html`<span class="label">${this.label}</span>${this.schema.value
? `: ${this.schema.value}`
: ""}`;
}
static get styles(): CSSResultGroup {

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

@@ -25,6 +25,8 @@ import { HomeAssistant } from "../../types";
const getValue = (obj, item) =>
obj ? (!item.name ? obj : obj[item.name]) : null;
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
let selectorImported = false;
@customElement("ha-form")
@@ -84,7 +86,7 @@ export class HaForm extends LitElement implements HaFormElement {
`
: ""}
${this.schema.map((item) => {
const error = getValue(this.error, item);
const error = getError(this.error, item);
return html`
${error
@@ -104,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,
@@ -113,6 +116,7 @@ export class HaForm extends LitElement implements HaFormElement {
hass: this.hass,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
context: this._generateContext(item),
})}
`;
})}
@@ -120,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 {
@@ -40,7 +41,7 @@ export interface HaFormSelector extends HaFormBaseSchema {
export interface HaFormConstantSchema extends HaFormBaseSchema {
type: "constant";
value: string;
value?: string;
}
export interface HaFormIntegerSchema extends HaFormBaseSchema {

View File

@@ -44,6 +44,9 @@ export class HaSelect extends SelectBase {
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
color: var(--secondary-text-color);
}
.mdc-select__anchor {
width: var(--ha-select-min-width, 200px);
}
`,
];
}

View File

@@ -28,7 +28,11 @@ 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;
});
}
}
}
@@ -85,12 +89,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 { AttributeSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-selector-attribute")
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@@ -17,11 +18,16 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled = false;
@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}
@@ -29,6 +35,47 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
></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,58 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { ColorRGBSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
import { hex2rgb, rgb2hex } from "../../common/color/convert-color";
import "../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;
protected render() {
return html`
<ha-textfield
type="color"
.value=${this.value ? rgb2hex(this.value as any) : ""}
.label=${this.label || ""}
@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,58 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { ColorTempSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
import "../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;
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}
@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,36 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { DateSelector } from "../../data/selector";
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;
protected render() {
return html`
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.value=${this.value}
>
</ha-date-input>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-date": HaDateSelector;
}
}

View File

@@ -0,0 +1,74 @@
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { DateTimeSelector } from "../../data/selector";
import type { HaDateInput } from "../ha-date-input";
import type { HaTimeInput } from "../ha-time-input";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-date-input";
import "../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;
@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}
.value=${values?.[0]}
@value-changed=${this._valueChanged}
>
</ha-date-input>
<ha-time-input
enable-second
.value=${values?.[1] || "0:00:00"}
.locale=${this.hass.locale}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
></ha-time-input>
`;
}
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 {
@@ -24,26 +25,46 @@ export class HaDeviceSelector extends LitElement {
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}
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}
></ha-devices-picker>
`;
}
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
@@ -71,12 +92,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 {

View File

@@ -6,6 +6,7 @@ import { subscribeEntityRegistry } from "../../data/entity_registry";
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")
@@ -23,14 +24,29 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled = false;
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.includeEntities}
.excludeEntities=${this.selector.entity.excludeEntities}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
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.includeEntities}
.excludeEntities=${this.selector.entity.excludeEntities}
></ha-entities-picker>
`;
}
public hassSubscribe(): UnsubscribeFunc[] {

View File

@@ -0,0 +1,80 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
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";
@customElement("ha-selector-location")
export class HaLocationSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: LocationSelector;
@property() public value?: LocationSelectorValue;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<ha-locations-editor
class="flex"
.hass=${this.hass}
.locations=${this._location(this.selector, this.value)}
@location-updated=${this._locationChanged}
@radius-updated=${this._radiusChanged}
></ha-locations-editor>
`;
}
private _location = memoizeOne(
(
selector: LocationSelector,
value?: LocationSelectorValue
): MarkerLocation[] => {
const computedStyles = getComputedStyle(this);
const zoneRadiusColor = selector.location.radius
? computedStyles.getPropertyValue("--zone-radius-color") ||
computedStyles.getPropertyValue("--accent-color")
: undefined;
return [
{
id: "location",
latitude: value?.latitude || this.hass.config.latitude,
longitude: value?.longitude || this.hass.config.longitude,
radius: selector.location.radius ? value?.radius || 1000 : undefined,
radius_color: zoneRadiusColor,
icon: selector.location.icon,
location_editable: true,
radius_editable: true,
},
];
}
);
private _locationChanged(ev: CustomEvent) {
const [latitude, longitude] = ev.detail.location;
fireEvent(this, "value-changed", {
value: { ...this.value, latitude, longitude },
});
}
private _radiusChanged(ev: CustomEvent) {
const radius = ev.detail.radius;
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-location": HaLocationSelector;
}
}

View File

@@ -46,7 +46,7 @@ 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}
.disabled=${this.disabled}
.required=${this.required}

View File

@@ -1,17 +1,19 @@
import "@material/mwc-formfield/mwc-formfield";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { 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-select";
import "../ha-radio";
@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;
@@ -21,24 +23,51 @@ 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>`;
protected render() {
if (this.required && this.selector.select.options!.length < 6) {
return html`
<div>
${this.label}
${this.selector.select.options.map((item: string | SelectOption) => {
const value = typeof item === "object" ? item.value : item;
const label = typeof item === "object" ? item.label : item;
return html`
<mwc-formfield .label=${label}>
<ha-radio
.checked=${value === this.value}
.value=${value}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-radio>
</mwc-formfield>
`;
})}
</div>
`;
}
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;
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
})}
</ha-select>
`;
}
private _valueChanged(ev) {
@@ -56,6 +85,9 @@ export class HaSelectSelector extends LitElement {
ha-select {
width: 100%;
}
mwc-formfield {
display: block;
}
`;
}
}

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

@@ -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";
@@ -20,6 +23,8 @@ import "./ha-selector-time";
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 {
@@ -39,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();
}
@@ -58,6 +65,7 @@ export class HaSelector extends LitElement {
disabled: this.disabled,
required: this.required,
helper: this.helper,
context: this.context,
id: "selector",
})}
`;

View File

@@ -9,6 +9,12 @@ export class HaTextField extends TextFieldBase {
@property({ attribute: "error-message" }) public errorMessage?: string;
// @ts-ignore
@property({ type: Boolean }) public icon?: boolean;
// @ts-ignore
@property({ type: Boolean }) public iconTrailing?: boolean;
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (
@@ -53,6 +59,11 @@ export class HaTextField extends TextFieldBase {
padding-right: var(--text-field-suffix-padding-right, 0px);
}
.mdc-text-field:not(.mdc-text-field--disabled)
.mdc-text-field__affix--suffix {
color: var(--secondary-text-color);
}
.mdc-text-field__icon {
color: var(--secondary-text-color);
}

View File

@@ -31,6 +31,8 @@ export class HaYamlEditor extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public readOnly = false;
@state() private _yaml = "";
public setValue(value): void {
@@ -61,6 +63,7 @@ export class HaYamlEditor extends LitElement {
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}
.readOnly=${this.readOnly}
mode="yaml"
autocomplete-entities
.error=${this.isValid === false}

View File

@@ -35,7 +35,7 @@ class SearchInput extends LitElement {
.autofocus=${this.autofocus}
.label=${this.label || "Search"}
.value=${this.filter || ""}
.icon=${true}
icon
.iconTrailing=${this.filter || this.suffix}
@input=${this._filterInputChanged}
>

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

@@ -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,

View File

@@ -65,8 +65,14 @@ export const ignoreConfigFlow = (
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
export const getConfigFlowHandlers = (
hass: HomeAssistant,
type?: "helper" | "integration"
) =>
hass.callApi<string[]>(
"GET",
`config/config_entries/flow_handlers${type ? `?type=${type}` : ""}`
);
export const fetchConfigFlowInProgress = (
conn: Connection

View File

@@ -28,7 +28,7 @@ export interface DataEntryFlowStepForm {
step_id: string;
data_schema: HaFormSchema[];
errors: Record<string, string>;
description_placeholders: Record<string, string>;
description_placeholders?: Record<string, string>;
last_step: boolean | null;
}
@@ -49,7 +49,7 @@ export interface DataEntryFlowStepCreateEntry {
title: string;
result?: ConfigEntry;
description: string;
description_placeholders: Record<string, string>;
description_placeholders?: Record<string, string>;
}
export interface DataEntryFlowStepAbort {
@@ -57,7 +57,7 @@ export interface DataEntryFlowStepAbort {
flow_id: string;
handler: string;
reason: string;
description_placeholders: Record<string, string>;
description_placeholders?: Record<string, string>;
}
export interface DataEntryFlowStepProgress {
@@ -66,7 +66,17 @@ export interface DataEntryFlowStepProgress {
handler: string;
step_id: string;
progress_action: string;
description_placeholders: Record<string, string>;
description_placeholders?: Record<string, string>;
}
export interface DataEntryFlowStepMenu {
type: "menu";
flow_id: string;
handler: string;
step_id: string;
/** If array, use value to lookup translations in strings.json */
menu_options: string[] | Record<string, string>;
description_placeholders?: Record<string, string>;
}
export type DataEntryFlowStep =
@@ -74,7 +84,8 @@ export type DataEntryFlowStep =
| DataEntryFlowStepExternal
| DataEntryFlowStepCreateEntry
| DataEntryFlowStepAbort
| DataEntryFlowStepProgress;
| DataEntryFlowStepProgress
| DataEntryFlowStepMenu;
export const subscribeDataEntryFlowProgressed = (
conn: Connection,

View File

@@ -12,7 +12,12 @@ import { subscribeOne } from "../common/util/subscribe-one";
import { HomeAssistant } from "../types";
import { ConfigEntry, getConfigEntries } from "./config_entries";
import { subscribeEntityRegistry } from "./entity_registry";
import { fetchStatistics, Statistics } from "./history";
import {
fetchStatistics,
Statistics,
StatisticsMetaData,
getStatisticMetadata,
} from "./history";
const energyCollectionKeys: (string | undefined)[] = [];
@@ -136,6 +141,7 @@ export interface GasSourceTypeEnergyPreference {
entity_energy_from: string | null;
entity_energy_price: string | null;
number_energy_price: number | null;
unit_of_measurement?: string | null;
}
type EnergySource =
@@ -241,14 +247,14 @@ const getEnergyData = async (
end?: Date
): Promise<EnergyData> => {
const [configEntries, entityRegistryEntries, info] = await Promise.all([
getConfigEntries(hass),
getConfigEntries(hass, { domain: "co2signal" }),
subscribeOne(hass.connection, subscribeEntityRegistry),
getEnergyInfo(hass),
]);
const co2SignalConfigEntry = configEntries.find(
(entry) => entry.domain === "co2signal"
);
const co2SignalConfigEntry = configEntries.length
? configEntries[0]
: undefined;
let co2SignalEntity: string | undefined;
@@ -271,6 +277,15 @@ const getEnergyData = async (
const consumptionStatIDs: string[] = [];
const statIDs: string[] = [];
const gasSources: GasSourceTypeEnergyPreference[] =
prefs.energy_sources.filter(
(source) => source.type === "gas"
) as GasSourceTypeEnergyPreference[];
const gasStatisticIdsWithMeta: StatisticsMetaData[] =
await getStatisticMetadata(
hass,
gasSources.map((source) => source.stat_energy_from)
);
for (const source of prefs.energy_sources) {
if (source.type === "solar") {
@@ -280,6 +295,20 @@ const getEnergyData = async (
if (source.type === "gas") {
statIDs.push(source.stat_energy_from);
const entity = hass.states[source.stat_energy_from];
if (!entity) {
for (const statisticIdWithMeta of gasStatisticIdsWithMeta) {
if (
statisticIdWithMeta?.statistic_id === source.stat_energy_from &&
statisticIdWithMeta?.unit_of_measurement
) {
source.unit_of_measurement =
statisticIdWithMeta?.unit_of_measurement === "Wh"
? "kWh"
: statisticIdWithMeta?.unit_of_measurement;
}
}
}
if (source.stat_cost) {
statIDs.push(source.stat_cost);
}
@@ -559,6 +588,9 @@ export const getEnergyGasUnit = (
? "kWh"
: entity.attributes.unit_of_measurement;
}
if (source.unit_of_measurement) {
return source.unit_of_measurement;
}
}
return undefined;
};

View File

@@ -14,6 +14,7 @@ export interface EntityRegistryEntry {
device_id: string | null;
area_id: string | null;
disabled_by: string | null;
hidden_by: string | null;
entity_category: "config" | "diagnostic" | null;
}
@@ -38,6 +39,7 @@ export interface EntityRegistryEntryUpdateParams {
device_class?: string | null;
area_id?: string | null;
disabled_by?: string | null;
hidden_by: string | null;
new_entity_id?: string;
}

71
src/data/helpers_crud.ts Normal file
View File

@@ -0,0 +1,71 @@
import { fetchCounter, updateCounter, deleteCounter } from "./counter";
import {
fetchInputBoolean,
updateInputBoolean,
deleteInputBoolean,
} from "./input_boolean";
import {
fetchInputButton,
updateInputButton,
deleteInputButton,
} from "./input_button";
import {
fetchInputDateTime,
updateInputDateTime,
deleteInputDateTime,
} from "./input_datetime";
import {
fetchInputNumber,
updateInputNumber,
deleteInputNumber,
} from "./input_number";
import {
fetchInputSelect,
updateInputSelect,
deleteInputSelect,
} from "./input_select";
import { fetchInputText, updateInputText, deleteInputText } from "./input_text";
import { fetchTimer, updateTimer, deleteTimer } from "./timer";
export const HELPERS_CRUD = {
input_boolean: {
fetch: fetchInputBoolean,
update: updateInputBoolean,
delete: deleteInputBoolean,
},
input_button: {
fetch: fetchInputButton,
update: updateInputButton,
delete: deleteInputButton,
},
input_text: {
fetch: fetchInputText,
update: updateInputText,
delete: deleteInputText,
},
input_number: {
fetch: fetchInputNumber,
update: updateInputNumber,
delete: deleteInputNumber,
},
input_datetime: {
fetch: fetchInputDateTime,
update: updateInputDateTime,
delete: deleteInputDateTime,
},
input_select: {
fetch: fetchInputSelect,
update: updateInputSelect,
delete: deleteInputSelect,
},
counter: {
fetch: fetchCounter,
update: updateCounter,
delete: deleteCounter,
},
timer: {
fetch: fetchTimer,
update: updateTimer,
delete: deleteTimer,
},
};

View File

@@ -1,4 +1,5 @@
import { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
@@ -7,6 +8,13 @@ import { HomeAssistant } from "../types";
import { FrontendLocaleData } from "./translation";
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
const NEED_ATTRIBUTE_DOMAINS = [
"climate",
"humidifier",
"input_datetime",
"thermostat",
"water_heater",
];
const LINE_ATTRIBUTES_TO_KEEP = [
"temperature",
"current_temperature",
@@ -131,6 +139,13 @@ export interface StatisticsValidationResults {
[statisticId: string]: StatisticsValidationResult[];
}
export const entityIdHistoryNeedsAttributes = (
hass: HomeAssistant,
entityId: string
) =>
!hass.states[entityId] ||
NEED_ATTRIBUTE_DOMAINS.includes(computeDomain(entityId));
export const fetchRecent = (
hass: HomeAssistant,
entityId: string,
@@ -138,7 +153,8 @@ export const fetchRecent = (
endTime: Date,
skipInitialState = false,
significantChangesOnly?: boolean,
minimalResponse = true
minimalResponse = true,
noAttributes?: boolean
): Promise<HassEntity[][]> => {
let url = "history/period";
if (startTime) {
@@ -157,7 +173,9 @@ export const fetchRecent = (
if (minimalResponse) {
url += "&minimal_response";
}
if (noAttributes) {
url += "&no_attributes";
}
return hass.callApi("GET", url);
};
@@ -171,6 +189,10 @@ export const fetchDate = (
"GET",
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${
entityId ? `&filter_entity_id=${entityId}` : ``
}${
entityId && !entityIdHistoryNeedsAttributes(hass, entityId)
? `&no_attributes`
: ``
}`
);
@@ -278,6 +300,10 @@ const processLineChartEntities = (
};
};
const stateUsesUnits = (state: HassEntity) =>
"unit_of_measurement" in state.attributes ||
"state_class" in state.attributes;
export const computeHistory = (
hass: HomeAssistant,
stateHistory: HassEntity[][],
@@ -294,16 +320,18 @@ export const computeHistory = (
return;
}
const stateWithUnitorStateClass = stateInfo.find(
(state) =>
state.attributes &&
("unit_of_measurement" in state.attributes ||
"state_class" in state.attributes)
);
const entityId = stateInfo[0].entity_id;
const currentState =
entityId in hass.states ? hass.states[entityId] : undefined;
const stateWithUnitorStateClass =
!currentState &&
stateInfo.find((state) => state.attributes && stateUsesUnits(state));
let unit: string | undefined;
if (stateWithUnitorStateClass) {
if (currentState && stateUsesUnits(currentState)) {
unit = currentState.attributes.unit_of_measurement || " ";
} else if (stateWithUnitorStateClass) {
unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " ";
} else {
unit = {
@@ -313,7 +341,7 @@ export const computeHistory = (
input_number: "#",
number: "#",
water_heater: hass.config.unit_system.temperature,
}[computeStateDomain(stateInfo[0])];
}[computeDomain(entityId)];
}
if (!unit) {
@@ -345,6 +373,15 @@ export const getStatisticIds = (
statistic_type,
});
export const getStatisticMetadata = (
hass: HomeAssistant,
statistic_ids?: string[]
) =>
hass.callWS<StatisticsMetaData[]>({
type: "recorder/get_statistics_metadata",
statistic_ids,
});
export const fetchStatistics = (
hass: HomeAssistant,
startTime: Date,
@@ -428,3 +465,16 @@ export const statisticsHaveType = (
stats: StatisticValue[],
type: StatisticType
) => stats.some((stat) => stat[type] !== null);
export const adjustStatisticsSum = (
hass: HomeAssistant,
statistic_id: string,
start_time: string,
adjustment: number
): Promise<void> =>
hass.callWS({
type: "recorder/adjust_sum_statistics",
statistic_id,
start_time,
adjustment,
});

View File

@@ -29,7 +29,7 @@ export const createImage = async (
body: fd,
});
if (resp.status === 413) {
throw new Error("Uploaded image is too large");
throw new Error(`Uploaded image is too large (${file.name})`);
} else if (resp.status !== 200) {
throw new Error("Unknown error");
}

View File

@@ -43,7 +43,7 @@ export const uploadLocalMedia = async (
}
);
if (resp.status === 413) {
throw new Error("Uploaded image is too large");
throw new Error(`Uploaded file is too large (${file.name})`);
} else if (resp.status !== 200) {
throw new Error("Unknown error");
}

View File

@@ -1,213 +0,0 @@
import { HomeAssistant } from "../types";
import { DeviceRegistryEntry } from "./device_registry";
export interface OZWNodeIdentifiers {
ozw_instance: number;
node_id: number;
}
export interface OZWDevice {
node_id: number;
node_query_stage: string;
is_awake: boolean;
is_failed: boolean;
is_zwave_plus: boolean;
ozw_instance: number;
event: string;
node_manufacturer_name: string;
node_product_name: string;
}
export interface OZWDeviceMetaDataResponse {
node_id: number;
ozw_instance: number;
metadata: OZWDeviceMetaData;
}
export interface OZWDeviceMetaData {
OZWInfoURL: string;
ZWAProductURL: string;
ProductPic: string;
Description: string;
ProductManualURL: string;
ProductPageURL: string;
InclusionHelp: string;
ExclusionHelp: string;
ResetHelp: string;
WakeupHelp: string;
ProductSupportURL: string;
Frequency: string;
Name: string;
ProductPicBase64: string;
}
export interface OZWInstance {
ozw_instance: number;
OZWDaemon_Version: string;
OpenZWave_Version: string;
QTOpenZWave_Version: string;
Status: string;
getControllerPath: string;
homeID: string;
}
export interface OZWNetworkStatistics {
ozw_instance: number;
node_count: number;
readCnt: number;
writeCnt: number;
ACKCnt: number;
CANCnt: number;
NAKCnt: number;
dropped: number;
retries: number;
}
export interface OZWDeviceConfig {
label: string;
type: string;
value: string | number;
parameter: number;
min: number;
max: number;
help: string;
}
export const nodeQueryStages = [
"ProtocolInfo",
"Probe",
"WakeUp",
"ManufacturerSpecific1",
"NodeInfo",
"NodePlusInfo",
"ManufacturerSpecific2",
"Versions",
"Instances",
"Static",
"CacheLoad",
"Associations",
"Neighbors",
"Session",
"Dynamic",
"Configuration",
"Complete",
];
export const networkOnlineStatuses = [
"driverAllNodesQueried",
"driverAllNodesQueriedSomeDead",
"driverAwakeNodesQueried",
];
export const networkStartingStatuses = [
"starting",
"started",
"Ready",
"driverReady",
];
export const networkOfflineStatuses = [
"Offline",
"stopped",
"driverFailed",
"driverReset",
"driverRemoved",
"driverAllNodesOnFire",
];
export const getIdentifiersFromDevice = function (
device: DeviceRegistryEntry
): OZWNodeIdentifiers | undefined {
if (!device) {
return undefined;
}
const ozwIdentifier = device.identifiers.find(
(identifier) => identifier[0] === "ozw"
);
if (!ozwIdentifier) {
return undefined;
}
const identifiers = ozwIdentifier[1].split(".");
return {
node_id: parseInt(identifiers[1]),
ozw_instance: parseInt(identifiers[0]),
};
};
export const fetchOZWInstances = (
hass: HomeAssistant
): Promise<OZWInstance[]> =>
hass.callWS({
type: "ozw/get_instances",
});
export const fetchOZWNetworkStatus = (
hass: HomeAssistant,
ozw_instance: number
): Promise<OZWInstance> =>
hass.callWS({
type: "ozw/network_status",
ozw_instance,
});
export const fetchOZWNetworkStatistics = (
hass: HomeAssistant,
ozw_instance: number
): Promise<OZWNetworkStatistics> =>
hass.callWS({
type: "ozw/network_statistics",
ozw_instance,
});
export const fetchOZWNodes = (
hass: HomeAssistant,
ozw_instance: number
): Promise<OZWDevice[]> =>
hass.callWS({
type: "ozw/get_nodes",
ozw_instance,
});
export const fetchOZWNodeStatus = (
hass: HomeAssistant,
ozw_instance: number,
node_id: number
): Promise<OZWDevice> =>
hass.callWS({
type: "ozw/node_status",
ozw_instance,
node_id,
});
export const fetchOZWNodeMetadata = (
hass: HomeAssistant,
ozw_instance: number,
node_id: number
): Promise<OZWDeviceMetaDataResponse> =>
hass.callWS({
type: "ozw/node_metadata",
ozw_instance,
node_id,
});
export const fetchOZWNodeConfig = (
hass: HomeAssistant,
ozw_instance: number,
node_id: number
): Promise<OZWDeviceConfig[]> =>
hass.callWS({
type: "ozw/get_config_parameters",
ozw_instance,
node_id,
});
export const refreshNodeInfo = (
hass: HomeAssistant,
ozw_instance: number,
node_id: number
): Promise<OZWDevice> =>
hass.callWS({
type: "ozw/refresh_node_info",
ozw_instance,
node_id,
});

View File

@@ -2,6 +2,8 @@ export type Selector =
| AddonSelector
| AttributeSelector
| EntitySelector
| DateSelector
| DateTimeSelector
| DeviceSelector
| DurationSelector
| AreaSelector
@@ -15,22 +17,43 @@ export type Selector =
| SelectSelector
| IconSelector
| MediaSelector
| ThemeSelector;
| ThemeSelector
| LocationSelector
| ColorTempSelector
| ColorRGBSelector;
export interface EntitySelector {
entity: {
integration?: string;
domain?: string | string[];
device_class?: string;
multiple?: boolean;
includeEntities?: string[];
excludeEntities?: string[];
};
}
export interface AttributeSelector {
attribute: {
entity_id: string;
entity_id?: string;
};
}
export interface ColorRGBSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
color_rgb: {};
}
export interface DateSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
date: {};
}
export interface DateTimeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
datetime: {};
}
export interface DeviceSelector {
device: {
integration?: string;
@@ -40,6 +63,7 @@ export interface DeviceSelector {
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
multiple?: boolean;
};
}
@@ -95,6 +119,13 @@ export interface NumberSelector {
};
}
export interface ColorTempSelector {
color_temp: {
min_mireds?: number;
max_mireds?: number;
};
}
export interface BooleanSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
boolean: {};
@@ -164,6 +195,16 @@ export interface MediaSelector {
media: {};
}
export interface LocationSelector {
location: { radius?: boolean; icon?: string };
}
export interface LocationSelectorValue {
latitude: number;
longitude: number;
radius?: number;
}
export interface MediaSelectorValue {
entity_id?: string;
media_content_id?: string;

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

@@ -0,0 +1,36 @@
import type {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { supportsFeature } from "../common/entity/supports-feature";
export const UPDATE_SUPPORT_INSTALL = 1;
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
export const UPDATE_SUPPORT_PROGRESS = 4;
export const UPDATE_SUPPORT_BACKUP = 8;
interface UpdateEntityAttributes extends HassEntityAttributeBase {
current_version: string | null;
in_progress: boolean | number;
latest_version: string | null;
release_summary: string | null;
release_url: string | null;
skipped_version: string | null;
title: string | null;
}
export interface UpdateEntity extends HassEntityBase {
attributes: UpdateEntityAttributes;
}
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
typeof entity.attributes.in_progress === "number";
export const updateCanInstall = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UPDATE_SUPPORT_INSTALL) &&
entity.attributes.latest_version !== entity.attributes.current_version &&
entity.attributes.latest_version !== entity.attributes.skipped_version;
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
updateUsesProgress(entity) || !!entity.attributes.in_progress;

View File

@@ -12,12 +12,12 @@ export interface Zone {
}
export interface ZoneMutableParams {
name: string;
icon?: string;
latitude: number;
longitude: number;
name: string;
passive: boolean;
radius: number;
passive?: boolean;
radius?: number;
}
export const fetchZones = (hass: HomeAssistant) =>

View File

@@ -1,81 +0,0 @@
import { HomeAssistant } from "../types";
export interface ZWaveNetworkStatus {
state: number;
}
export interface ZWaveValue {
key: number;
value: {
index: number;
instance: number;
label: string;
poll_intensity: number;
};
}
export interface ZWaveConfigItem {
key: number;
value: {
data: any;
data_items: any[];
help: string;
label: string;
max: number;
min: number;
type: string;
};
}
export interface ZWaveConfigServiceData {
node_id: number;
parameter: number;
value: number | string;
}
export interface ZWaveNode {
attributes: ZWaveAttributes;
}
export interface ZWaveAttributes {
node_id: number;
wake_up_interval?: number;
}
export interface ZWaveMigrationConfig {
usb_path: string;
network_key: string;
}
export const ZWAVE_NETWORK_STATE_STOPPED = 0;
export const ZWAVE_NETWORK_STATE_FAILED = 1;
export const ZWAVE_NETWORK_STATE_STARTED = 5;
export const ZWAVE_NETWORK_STATE_AWAKED = 7;
export const ZWAVE_NETWORK_STATE_READY = 10;
export const fetchNetworkStatus = (
hass: HomeAssistant
): Promise<ZWaveNetworkStatus> =>
hass.callWS({
type: "zwave/network_status",
});
export const startZwaveJsConfigFlow = (
hass: HomeAssistant
): Promise<{ flow_id: string }> =>
hass.callWS({
type: "zwave/start_zwave_js_config_flow",
});
export const fetchMigrationConfig = (
hass: HomeAssistant
): Promise<ZWaveMigrationConfig> =>
hass.callWS({
type: "zwave/get_migration_config",
});
export const fetchValues = (hass: HomeAssistant, nodeId: number) =>
hass.callApi<ZWaveValue[]>("GET", `zwave/values/${nodeId}`);
export const fetchNodeConfig = (hass: HomeAssistant, nodeId: number) =>
hass.callApi<ZWaveConfigItem[]>("GET", `zwave/config/${nodeId}`);

View File

@@ -1,5 +1,5 @@
import "@material/mwc-button";
import { mdiClose } from "@mdi/js";
import { mdiClose, mdiHelpCircle } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@@ -33,6 +33,7 @@ import {
} from "../../data/device_registry";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { showAlertDialog } from "../generic/show-dialog-box";
import {
DataEntryFlowDialogParams,
@@ -46,6 +47,7 @@ import "./step-flow-loading";
import "./step-flow-pick-flow";
import "./step-flow-pick-handler";
import "./step-flow-progress";
import "./step-flow-menu";
let instance = 0;
@@ -235,14 +237,33 @@ class DataEntryFlowDialog extends LitElement {
// to reset the element.
""
: html`
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.dismiss"
)}
.path=${mdiClose}
dialogAction="close"
?rtl=${computeRTL(this.hass)}
></ha-icon-button>
<div class="dialog-actions">
${this._step
? html`
<a
href=${documentationUrl(
this.hass,
`/integrations/${this._step.handler}`
)}
target="_blank"
rel="noreferrer noopener"
><ha-icon-button
.label=${this.hass.localize("ui.common.help")}
.path=${mdiHelpCircle}
?rtl=${computeRTL(this.hass)}
></ha-icon-button
></a>
`
: ""}
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.dismiss"
)}
.path=${mdiClose}
dialogAction="close"
?rtl=${computeRTL(this.hass)}
></ha-icon-button>
</div>
${this._step === null
? this._handler
? html`<step-flow-pick-flow
@@ -292,6 +313,14 @@ class DataEntryFlowDialog extends LitElement {
.hass=${this.hass}
></step-flow-progress>
`
: this._step.type === "menu"
? html`
<step-flow-menu
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-menu>
`
: this._devices === undefined || this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry
html`
@@ -377,13 +406,20 @@ class DataEntryFlowDialog extends LitElement {
step = await this._params!.flowConfig.createFlow(this.hass, handler);
} catch (err: any) {
this.closeDialog();
const message =
err?.status_code === 404
? this.hass.localize(
"ui.panel.config.integrations.config_flow.no_config_flow"
)
: `${this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
)}: ${err?.body?.message || err?.message}`;
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: `${this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
)}: ${err.message || err.body}`,
text: message,
});
return;
} finally {
@@ -414,7 +450,7 @@ class DataEntryFlowDialog extends LitElement {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: err.message || err.body,
text: err?.body?.message,
});
return;
} finally {
@@ -456,16 +492,19 @@ class DataEntryFlowDialog extends LitElement {
ha-dialog {
--dialog-content-padding: 0;
}
ha-icon-button {
.dialog-actions {
padding: 16px;
position: absolute;
top: 0;
right: 0;
}
ha-icon-button[rtl] {
.dialog-actions[rtl] {
right: auto;
left: 0;
}
.dialog-actions > * {
color: var(--secondary-text-color);
}
`,
];
}

View File

@@ -24,7 +24,7 @@ export const showConfigFlowDialog = (
loadDevicesAndAreas: true,
getFlowHandlers: async (hass) => {
const [handlers] = await Promise.all([
getConfigFlowHandlers(hass),
getConfigFlowHandlers(hass, "integration"),
hass.loadBackendTranslation("title", undefined, true),
]);
@@ -181,6 +181,21 @@ export const showConfigFlowDialog = (
: "";
},
renderMenuHeader(hass, step) {
return (
hass.localize(
`component.${step.handler}.config.step.${step.step_id}.title`
) || hass.localize(`component.${step.handler}.title`)
);
},
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.menu_options.${option}`,
step.description_placeholders
);
},
renderLoadingDescription(hass, reason, handler, step) {
if (!["loading_flow", "loading_step"].includes(reason)) {
return "";

View File

@@ -7,6 +7,7 @@ import {
DataEntryFlowStepCreateEntry,
DataEntryFlowStepExternal,
DataEntryFlowStepForm,
DataEntryFlowStepMenu,
DataEntryFlowStepProgress,
} from "../../data/data_entry_flow";
import { HomeAssistant } from "../../types";
@@ -80,6 +81,14 @@ export interface FlowConfig {
step: DataEntryFlowStepProgress
): TemplateResult | "";
renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string;
renderMenuOption(
hass: HomeAssistant,
step: DataEntryFlowStepMenu,
option: string
): string;
renderLoadingDescription(
hass: HomeAssistant,
loadingReason: LoadingReason,

View File

@@ -134,6 +134,21 @@ export const showOptionsFlowDialog = (
: "";
},
renderMenuHeader(hass, step) {
return (
hass.localize(
`component.${step.handler}.option.step.${step.step_id}.title`
) || hass.localize(`component.${step.handler}.title`)
);
},
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`,
step.description_placeholders
);
},
renderLoadingDescription(hass, reason) {
return (
hass.localize(`component.${configEntry.domain}.options.loading`) ||

View File

@@ -0,0 +1,80 @@
import "@material/mwc-list/mwc-list-item";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import type { DataEntryFlowStepMenu } from "../../data/data_entry_flow";
import type { HomeAssistant } from "../../types";
import type { FlowConfig } from "./show-dialog-data-entry-flow";
import "../../components/ha-icon-next";
import { configFlowContentStyles } from "./styles";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("step-flow-menu")
class StepFlowMenu extends LitElement {
@property({ attribute: false }) public flowConfig!: FlowConfig;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public step!: DataEntryFlowStepMenu;
protected render(): TemplateResult {
let options: string[];
let translations: Record<string, string>;
if (Array.isArray(this.step.menu_options)) {
options = this.step.menu_options;
translations = {};
for (const option of options) {
translations[option] = this.flowConfig.renderMenuOption(
this.hass,
this.step,
option
);
}
} else {
options = Object.keys(this.step.menu_options);
translations = this.step.menu_options;
}
return html`
<h2>${this.flowConfig.renderMenuHeader(this.hass, this.step)}</h2>
<div class="options">
${options.map(
(option) => html`
<mwc-list-item hasMeta .step=${option} @click=${this._handleStep}>
<span>${translations[option]}</span>
<ha-icon-next slot="meta"></ha-icon-next>
</mwc-list-item>
`
)}
</div>
`;
}
private _handleStep(ev) {
fireEvent(this, "flow-update", {
stepPromise: this.flowConfig.handleFlowStep(
this.hass,
this.step.flow_id,
{
next_step_id: ev.currentTarget.step,
}
),
});
}
static styles = [
configFlowContentStyles,
css`
.options {
margin-top: 20px;
margin-bottom: 8px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"step-flow-menu": StepFlowMenu;
}
}

View File

@@ -216,15 +216,16 @@ class StepFlowPickHandler extends LitElement {
if (handler.is_add) {
if (handler.slug === "zwave_js") {
const entries = await getConfigEntries(this.hass);
const entry = entries.find((ent) => ent.domain === "zwave_js");
const entries = await getConfigEntries(this.hass, {
domain: "zwave_js",
});
if (!entry) {
if (!entries.length) {
return;
}
showZWaveJSAddNodeDialog(this, {
entry_id: entry.entry_id,
entry_id: entries[0].entry_id,
});
} else if (handler.slug === "zha") {
navigate("/config/zha/add");

View File

@@ -12,7 +12,7 @@ import {
import type { HomeAssistant } from "../../../types";
const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"];
const ARM_ACTIONS = ["arm_away", "arm_home"];
const ARM_ACTIONS = ["arm_home", "arm_away"];
const DISARM_ACTIONS = ["disarm"];
@customElement("more-info-alarm_control_panel")

View File

@@ -1,3 +1,4 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
@@ -12,6 +13,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-select";
import "../../../components/ha-slider";
import "../../../components/ha-switch";
import {
@@ -19,8 +21,6 @@ import {
HUMIDIFIER_SUPPORT_MODES,
} from "../../../data/humidifier";
import { HomeAssistant } from "../../../types";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
class MoreInfoHumidifier extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -67,26 +67,24 @@ class MoreInfoHumidifier extends LitElement {
${supportModes
? html`
<div class="container-modes">
<mwc-list
.label=${hass.localize("ui.card.humidifier.mode")}
.value=${stateObj.attributes.mode}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleModeChanged}
@closed=${stopPropagation}
>
${stateObj.attributes.available_modes!.map(
(mode) => html`
<mwc-list-item .value=${mode}>
${hass.localize(
`state_attributes.humidifier.mode.${mode}`
) || mode}
</mwc-list-item>
`
)}
</mwc-list>
</div>
<ha-select
.label=${hass.localize("ui.card.humidifier.mode")}
.value=${stateObj.attributes.mode}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleModeChanged}
@closed=${stopPropagation}
>
${stateObj.attributes.available_modes!.map(
(mode) => html`
<mwc-list-item .value=${mode}>
${hass.localize(
`state_attributes.humidifier.mode.${mode}`
) || mode}
</mwc-list-item>
`
)}
</ha-select>
`
: ""}
</div>

View File

@@ -0,0 +1,212 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-checkbox";
import "../../../components/ha-formfield";
import "../../../components/ha-markdown";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import {
updateIsInstalling,
UpdateEntity,
UPDATE_SUPPORT_BACKUP,
UPDATE_SUPPORT_INSTALL,
UPDATE_SUPPORT_PROGRESS,
UPDATE_SUPPORT_SPECIFIC_VERSION,
} from "../../../data/update";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-update")
class MoreInfoUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: UpdateEntity;
protected render(): TemplateResult {
if (
!this.hass ||
!this.stateObj ||
UNAVAILABLE_STATES.includes(this.stateObj.state)
) {
return html``;
}
const skippedVersion =
this.stateObj.attributes.latest_version &&
this.stateObj.attributes.skipped_version ===
this.stateObj.attributes.latest_version;
return html`
${this.stateObj.attributes.in_progress
? supportsFeature(this.stateObj, UPDATE_SUPPORT_PROGRESS) &&
typeof this.stateObj.attributes.in_progress === "number"
? html`<mwc-linear-progress
.progress=${this.stateObj.attributes.in_progress / 100}
buffer=""
></mwc-linear-progress>`
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
: ""}
${this.stateObj.attributes.title
? html`<h3>${this.stateObj.attributes.title}</h3>`
: ""}
<div class="row">
<div class="key">
${this.hass.localize(
"ui.dialogs.more_info_control.update.current_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.current_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
<div class="row">
<div class="key">
${this.hass.localize(
"ui.dialogs.more_info_control.update.latest_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.latest_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
${this.stateObj.attributes.release_url
? html`<div class="row">
<div class="key">
<a
href=${this.stateObj.attributes.release_url}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.release_announcement"
)}
</a>
</div>
</div>`
: ""}
${this.stateObj.attributes.release_summary
? html`<hr />
<ha-markdown
.content=${this.stateObj.attributes.release_summary}
></ha-markdown> `
: ""}
${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP)
? html`<hr />
<ha-formfield
.label=${this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup"
)}
>
<ha-checkbox
checked
.disabled=${updateIsInstalling(this.stateObj)}
></ha-checkbox>
</ha-formfield> `
: ""}
<hr />
<div class="actions">
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === "off" ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize("ui.dialogs.more_info_control.update.skip")}
</mwc-button>
${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL)
? html`
<mwc-button
@click=${this._handleInstall}
.disabled=${(this.stateObj.state === "off" &&
!skippedVersion) ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.install"
)}
</mwc-button>
`
: ""}
</div>
`;
}
get _shouldCreateBackup(): boolean | null {
if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) {
return null;
}
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) {
return checkbox.checked;
}
return true;
}
private _handleInstall(): void {
const installData: Record<string, any> = {
entity_id: this.stateObj!.entity_id,
};
if (this._shouldCreateBackup) {
installData.backup = true;
}
if (
supportsFeature(this.stateObj!, UPDATE_SUPPORT_SPECIFIC_VERSION) &&
this.stateObj!.attributes.latest_version
) {
installData.version = this.stateObj!.attributes.latest_version;
}
this.hass.callService("update", "install", installData);
}
private _handleSkip(): void {
this.hass.callService("update", "skip", {
entity_id: this.stateObj!.entity_id,
});
}
static get styles(): CSSResultGroup {
return css`
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
ha-expansion-panel {
margin: 16px 0;
}
.row {
margin: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.actions {
margin: 8px 0 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.actions mwc-button {
margin: 0 4px 4px;
}
a {
color: var(--primary-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-update": MoreInfoUpdate;
}
}

View File

@@ -25,6 +25,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
script: () => import("./controls/more-info-script"),
sun: () => import("./controls/more-info-sun"),
timer: () => import("./controls/more-info-timer"),
update: () => import("./controls/more-info-update"),
vacuum: () => import("./controls/more-info-vacuum"),
water_heater: () => import("./controls/more-info-water_heater"),
weather: () => import("./controls/more-info-weather"),

View File

@@ -162,7 +162,7 @@ export class QuickBar extends LitElement {
"ui.dialogs.quick-bar.filter_placeholder"
)}
.value=${this._commandMode ? `>${this._search}` : this._search}
.icon=${true}
icon
.iconTrailing=${this._search !== undefined || this._narrow}
@input=${this._handleSearchChange}
@keydown=${this._handleInputKeyDown}

View File

@@ -1,7 +1,6 @@
/* eslint-disable lit/prefer-static-styles */
import "@material/mwc-button/mwc-button";
import { mdiMicrophone } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResultGroup,
@@ -10,12 +9,16 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import { SpeechRecognition } from "../../common/dom/speech-recognition";
import { uid } from "../../common/util/uid";
import "../../components/ha-dialog";
import type { HaDialog } from "../../components/ha-dialog";
import "../../components/ha-icon-button";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import {
AgentInfo,
getAgentInfo,
@@ -24,9 +27,6 @@ import {
} from "../../data/conversation";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../../components/ha-dialog";
import type { HaDialog } from "../../components/ha-dialog";
import "@material/mwc-button/mwc-button";
interface Message {
who: string;
@@ -127,18 +127,19 @@ export class HaVoiceCommandDialog extends LitElement {
: ""}
</div>
<div class="input" slot="primaryAction">
<paper-input
<ha-textfield
@keyup=${this._handleKeyUp}
.label=${this.hass.localize(
`ui.dialogs.voice_command.${
SpeechRecognition ? "label_voice" : "label"
}`
)}
autofocus
dialogInitialFocus
iconTrailing
>
${SpeechRecognition
? html`
<span suffix="" slot="suffix">
<span slot="trailingIcon">
${this.results
? html`
<div class="bouncer">
@@ -155,7 +156,7 @@ export class HaVoiceCommandDialog extends LitElement {
</span>
`
: ""}
</paper-input>
</ha-textfield>
${this._agentInfo && this._agentInfo.attribution
? html`
<a
@@ -195,7 +196,7 @@ export class HaVoiceCommandDialog extends LitElement {
}
private _handleKeyUp(ev: KeyboardEvent) {
const input = ev.target as PaperInputElement;
const input = ev.target as HaTextField;
if (ev.keyCode === 13 && input.value) {
this._processText(input.value);
input.value = "";
@@ -327,6 +328,7 @@ export class HaVoiceCommandDialog extends LitElement {
css`
ha-icon-button {
color: var(--secondary-text-color);
margin-right: -24px;
}
ha-icon-button[active] {
@@ -338,7 +340,9 @@ export class HaVoiceCommandDialog extends LitElement {
--secondary-action-button-flex: 0;
--mdc-dialog-max-width: 450px;
}
ha-textfield {
display: block;
}
a.button {
text-decoration: none;
}
@@ -406,7 +410,6 @@ export class HaVoiceCommandDialog extends LitElement {
width: 48px;
height: 48px;
position: absolute;
top: 0;
}
.double-bounce1,
.double-bounce2 {

View File

@@ -300,7 +300,9 @@ export const provideHass = (
applyThemesOnElement(
document.documentElement,
themes,
selectedTheme!.theme
selectedTheme!.theme,
undefined,
true
);
},

View File

@@ -101,7 +101,8 @@ class SupervisorErrorScreen extends LitElement {
this.parentElement,
this.hass.themes,
themeName,
themeSettings
themeSettings,
true
);
}

View File

@@ -133,13 +133,19 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
import("./particles");
}
if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement(document.documentElement, {
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
});
applyThemesOnElement(
document.documentElement,
{
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
}

View File

@@ -169,8 +169,8 @@ class OnboardingIntegrations extends LitElement {
}
private async _loadConfigEntries() {
const entries = await getConfigEntries(this.hass!);
// We filter out the config entry for the local weather and rpi_power.
const entries = await getConfigEntries(this.hass!, { type: "integration" });
// We filter out the config entries that are automatically created during onboarding.
// It is one that we create automatically and it will confuse the user
// if it starts showing up during onboarding.
this._entries = entries.filter(

View File

@@ -1,7 +1,6 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-textarea";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-toggle";
@@ -11,6 +10,7 @@ import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown";
import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row";
import "../../../components/ha-textfield";
import {
BlueprintAutomationConfig,
triggerAutomationActions,
@@ -38,6 +38,8 @@ export class HaBlueprintAutomationEditor extends LitElement {
@state() private _blueprints?: Blueprints;
@state() private _showDescription = false;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._getBlueprints();
@@ -50,6 +52,17 @@ export class HaBlueprintAutomationEditor extends LitElement {
return this._blueprints[this.config.use_blueprint.path];
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (
!this._showDescription &&
changedProps.has("config") &&
this.config.description
) {
this._showDescription = true;
}
}
protected render() {
const blueprint = this._blueprint;
return html`
@@ -64,26 +77,39 @@ export class HaBlueprintAutomationEditor extends LitElement {
</span>
<ha-card>
<div class="card-content">
<paper-input
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
name="alias"
.value=${this.config.alias}
@value-changed=${this._valueChanged}
.value=${this.config.alias || ""}
@change=${this._valueChanged}
>
</paper-input>
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
.value=${this.config.description}
@value-changed=${this._valueChanged}
></paper-textarea>
</ha-textfield>
${this._showDescription
? html`
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
autogrow
.value=${this.config.description || ""}
@change=${this._valueChanged}
></ha-textarea>
`
: html`
<div class="link-button-row">
<button class="link" @click=${this._addDescription}>
${this.hass.localize(
"ui.panel.config.automation.editor.description.add"
)}
</button>
</div>
`}
</div>
${this.stateObj
? html`
@@ -173,15 +199,14 @@ export class HaBlueprintAutomationEditor extends LitElement {
value?.default}
@value-changed=${this._inputChanged}
></ha-selector>`
: html`<paper-input
: html`<ha-textfield
.key=${key}
required
.value=${(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]) ??
value?.default}
@value-changed=${this._inputChanged}
no-label-float
></paper-input>`}
@input=${this._inputChanged}
></ha-textfield>`}
</ha-settings-row>`
)
: html`<p class="padding">
@@ -221,7 +246,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
ev.stopPropagation();
const target = ev.target as any;
const key = target.key;
const value = ev.detail.value;
const value = ev.detail?.value || target.value;
if (
(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key] === value) ||
@@ -253,7 +278,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
if (!name) {
return;
}
const newVal = ev.detail.value;
const newVal = target.value;
if ((this.config![name] || "") === newVal) {
return;
}
@@ -262,6 +287,10 @@ export class HaBlueprintAutomationEditor extends LitElement {
});
}
private _addDescription() {
this._showDescription = true;
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -273,9 +302,16 @@ export class HaBlueprintAutomationEditor extends LitElement {
.padding {
padding: 16px;
}
.link-button-row {
padding: 14px;
}
.blueprint-picker-container {
padding: 0 16px 16px;
}
ha-textarea,
ha-textfield {
display: block;
}
h3 {
margin: 16px;
}
@@ -292,9 +328,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
--paper-time-input-justify-content: flex-end;
border-top: 1px solid var(--divider-color);
}
:host(:not([narrow])) ha-settings-row paper-input {
width: 60%;
}
:host(:not([narrow])) ha-settings-row ha-textfield,
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
}

View File

@@ -7,12 +7,18 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-icon-button";
import { Condition } from "../../../../data/automation";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { Condition, testCondition } from "../../../../data/automation";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-condition-editor";
import { validateConfig } from "../../../../data/config";
export interface ConditionElement extends LitElement {
condition: Condition;
@@ -61,6 +67,11 @@ export default class HaAutomationConditionRow extends LitElement {
<ha-card>
<div class="card-content">
<div class="card-menu">
<ha-progress-button @click=${this._testCondition}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)}
</ha-progress-button>
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button
slot="trigger"
@@ -165,6 +176,64 @@ export default class HaAutomationConditionRow extends LitElement {
this._yamlMode = !this._yamlMode;
}
private async _testCondition(ev) {
const condition = this.condition;
const button = ev.target as HaProgressButton;
if (button.progress) {
return;
}
button.progress = true;
try {
const validateResult = await validateConfig(this.hass, {
condition,
});
// Abort if condition changed.
if (this.condition !== condition) {
return;
}
if (!validateResult.condition.valid) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.conditions.invalid_condition"
),
text: validateResult.condition.error,
});
return;
}
let result: { result: boolean };
try {
result = await testCondition(this.hass, condition);
} catch (err: any) {
if (this.condition !== condition) {
return;
}
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.conditions.test_failed"
),
text: err.message,
});
return;
}
if (this.condition !== condition) {
return;
}
if (result.result) {
button.actionSuccess();
} else {
button.actionError();
}
} finally {
button.progress = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -173,6 +242,8 @@ export default class HaAutomationConditionRow extends LitElement {
float: right;
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex;
align-items: center;
}
.rtl .card-menu {
float: left;

View File

@@ -1,10 +1,11 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-circular-progress";
import "../../../../components/ha-dialog";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
import type { AutomationConfig } from "../../../../data/automation";
import { convertThingTalk } from "../../../../data/cloud";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
@@ -12,7 +13,6 @@ import type { HomeAssistant } from "../../../../types";
import "./ha-thingtalk-placeholders";
import type { PlaceholderValues } from "./ha-thingtalk-placeholders";
import type { ThingtalkDialogParams } from "./show-dialog-thingtalk";
import "../../../../components/ha-dialog";
export interface Placeholder {
name: string;
@@ -38,7 +38,7 @@ class DialogThingtalk extends LitElement {
@state() private _placeholders?: PlaceholderContainer;
@query("#input") private _input?: PaperInputElement;
@query("#input") private _input?: HaTextField;
private _value?: string;
@@ -58,7 +58,7 @@ class DialogThingtalk extends LitElement {
this._placeholders = undefined;
this._params = undefined;
if (this._input) {
this._input.value = null;
this._input.value = "";
}
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -127,13 +127,13 @@ class DialogThingtalk extends LitElement {
</button>
</li>
</ul>
<paper-input
<ha-textfield
id="input"
label="What should this automation do?"
.value=${this._value}
autofocus
@keyup=${this._handleKeyUp}
></paper-input>
></ha-textfield>
<a
href="https://almond.stanford.edu/"
target="_blank"

View File

@@ -16,12 +16,16 @@ import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-yaml-editor";
import "../../../../components/ha-select";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-textfield";
import { subscribeTrigger, Trigger } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "./types/ha-automation-trigger-device";
@@ -94,7 +98,7 @@ export default class HaAutomationTriggerRow extends LitElement {
@state() private _requestShowId = false;
@state() private _triggered = false;
@state() private _triggered?: Record<string, unknown>;
@state() private _triggerColor = false;
@@ -231,9 +235,10 @@ export default class HaAutomationTriggerRow extends LitElement {
</div>
<div
class="triggered ${classMap({
active: this._triggered,
active: this._triggered !== undefined,
accent: this._triggerColor,
})}"
@click=${this._showTriggeredInfo}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.triggered"
@@ -288,7 +293,7 @@ export default class HaAutomationTriggerRow extends LitElement {
}
const validateResult = await validateConfig(this.hass, {
trigger: this.trigger,
trigger,
});
// Don't do anything if trigger not valid or if trigger changed.
@@ -298,16 +303,16 @@ export default class HaAutomationTriggerRow extends LitElement {
const triggerUnsub = subscribeTrigger(
this.hass,
() => {
(result) => {
if (untriggerTimeout !== undefined) {
clearTimeout(untriggerTimeout);
this._triggerColor = !this._triggerColor;
} else {
this._triggerColor = false;
}
this._triggered = true;
this._triggered = result;
untriggerTimeout = window.setTimeout(() => {
this._triggered = false;
this._triggered = undefined;
untriggerTimeout = undefined;
}, showTriggeredTime);
},
@@ -416,6 +421,18 @@ export default class HaAutomationTriggerRow extends LitElement {
this._yamlMode = !this._yamlMode;
}
private _showTriggeredInfo() {
showAlertDialog(this, {
text: html`
<ha-yaml-editor
readOnly
.hass=${this.hass}
.defaultValue=${this._triggered}
></ha-yaml-editor>
`,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -426,12 +443,12 @@ export default class HaAutomationTriggerRow extends LitElement {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.triggered {
cursor: pointer;
position: absolute;
top: 0px;
right: 0px;
left: 0px;
text-transform: uppercase;
pointer-events: none;
font-weight: bold;
font-size: 14px;
background-color: var(--primary-color);
@@ -446,6 +463,9 @@ export default class HaAutomationTriggerRow extends LitElement {
.triggered.active {
max-height: 100px;
}
.triggered:hover {
opacity: 0.8;
}
.triggered.accent {
background-color: var(--accent-color);
color: var(--text-accent-color, var(--text-primary-color));

View File

@@ -88,7 +88,7 @@ export class HaWebhookTrigger extends LitElement {
.helper=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.webhook.webhook_id_helper"
)}
.iconTrailing=${true}
iconTrailing
.value=${webhookId || ""}
@input=${this._valueChanged}
>

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