Compare commits

...

83 Commits

Author SHA1 Message Date
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
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
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
126 changed files with 2568 additions and 6850 deletions

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: 67 KiB

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,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,7 @@ const SCHEMAS: {
},
boolean: { name: "Boolean", selector: { boolean: {} } },
time: { name: "Time", selector: { time: {} } },
date: { name: "Date", selector: { date: {} } },
action: { name: "Action", selector: { action: {} } },
text: {
name: "Text",
@@ -168,6 +169,23 @@ const SCHEMAS: {
},
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

@@ -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": "^6.1.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 = 20220316.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

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

@@ -9,7 +9,6 @@ import {
mdiCast,
mdiCastConnected,
mdiClock,
mdiEmoticonDead,
mdiFlash,
mdiGestureTapButton,
mdiLanConnect,
@@ -22,14 +21,11 @@ import {
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiSleep,
mdiTimerSand,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiWeatherNight,
mdiZWave,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
/**
@@ -115,18 +111,6 @@ export const domainIcon = (
return mdiFlash;
}
case "zwave":
switch (compareState) {
case "dead":
return mdiEmoticonDead;
case "sleeping":
return mdiSleep;
case "initializing":
return mdiTimerSand;
default:
return mdiZWave;
}
case "sensor": {
const icon = sensorIcon(stateObj);
if (icon) {

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

@@ -51,6 +51,8 @@ class HaEntitiesPickerLight extends LitElement {
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
@@ -94,7 +96,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

@@ -15,18 +15,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;
@@ -96,7 +99,7 @@ export class HaEntityPicker extends LitElement {
private _initedStates = false;
private _states: HassEntity[] = [];
private _states: HassEntityWithCachedName[] = [];
private _getStates = memoizeOne(
(
@@ -107,8 +110,8 @@ export class HaEntityPicker extends LitElement {
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
) => {
let states: HassEntity[] = [];
): HassEntityWithCachedName[] => {
let states: HassEntityWithCachedName[] = [];
if (!hass) {
return [];
@@ -122,7 +125,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"
),
@@ -190,7 +193,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"
),

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

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

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

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

@@ -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 {
@@ -30,20 +31,36 @@ export class HaDeviceSelector extends LitElement {
}
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 => {

View File

@@ -7,6 +7,7 @@ import { EntitySelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entity-picker";
import "../entity/ha-entities-picker";
@customElement("ha-selector-entity")
export class HaEntitySelector extends SubscribeMixin(LitElement) {
@@ -23,14 +24,25 @@ 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}
.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}
></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

@@ -8,6 +8,8 @@ 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-device";
import "./ha-selector-duration";
import "./ha-selector-entity";
@@ -20,6 +22,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 +43,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 +64,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

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

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

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

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,7 @@ export type Selector =
| AddonSelector
| AttributeSelector
| EntitySelector
| DateSelector
| DeviceSelector
| DurationSelector
| AreaSelector
@@ -15,22 +16,36 @@ 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;
};
}
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 DeviceSelector {
device: {
integration?: string;
@@ -40,6 +55,7 @@ export interface DeviceSelector {
domain?: EntitySelector["entity"]["domain"];
device_class?: EntitySelector["entity"]["device_class"];
};
multiple?: boolean;
};
}
@@ -95,6 +111,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 +187,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;

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

@@ -46,6 +46,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;
@@ -292,6 +293,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 +386,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 +430,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 {

View File

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

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

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

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

View File

@@ -1,11 +1,12 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { state } from "lit/decorators";
import { query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import type { HaTextField } from "../../../../components/ha-textfield";
import "../../../../components/ha-textfield";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { documentationUrl } from "../../../../util/documentation-url";
import { WebhookDialogParams } from "./show-dialog-manage-cloudhook";
@@ -17,6 +18,8 @@ export class DialogManageCloudhook extends LitElement {
@state() private _params?: WebhookDialogParams;
@query("ha-textfield") _input!: HaTextField;
public showDialog(params: WebhookDialogParams) {
this._params = params;
}
@@ -53,12 +56,12 @@ export class DialogManageCloudhook extends LitElement {
"ui.panel.config.cloud.dialog_cloudhook.available_at"
)}
</p>
<paper-input
label=${inputLabel}
value=${cloudhook.cloudhook_url}
<ha-textfield
.label=${inputLabel}
.value=${cloudhook.cloudhook_url}
@click=${this._copyClipboard}
@blur=${this._restoreLabel}
></paper-input>
></ha-textfield>
<p>
${cloudhook.managed
? html`
@@ -98,10 +101,6 @@ export class DialogManageCloudhook extends LitElement {
`;
}
private get _paperInput(): PaperInputElement {
return this.shadowRoot!.querySelector("paper-input")!;
}
private async _disableWebhook() {
showConfirmationDialog(this, {
text: this.hass!.localize(
@@ -117,14 +116,10 @@ export class DialogManageCloudhook extends LitElement {
}
private _copyClipboard(ev: FocusEvent) {
// paper-input -> iron-input -> input
const paperInput = ev.currentTarget as PaperInputElement;
const input = (paperInput.inputElement as any)
.inputElement as HTMLInputElement;
input.setSelectionRange(0, input.value.length);
const textField = ev.currentTarget as HaTextField;
try {
document.execCommand("copy");
paperInput.label = this.hass!.localize(
copyToClipboard(textField.value);
textField.label = this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.copied_to_clipboard"
);
} catch (err: any) {
@@ -133,18 +128,19 @@ export class DialogManageCloudhook extends LitElement {
}
private _restoreLabel() {
this._paperInput.label = inputLabel;
this._input.label = inputLabel;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-dialog {
width: 650px;
}
paper-input {
margin-top: -8px;
ha-textfield {
display: block;
}
button.link {
color: var(--primary-color);

View File

@@ -1,12 +1,25 @@
import "@material/mwc-button/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 {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-switch";
import "../../../components/ha-alert";
import "../../../components/ha-formfield";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { CloudStatus, fetchCloudStatus } from "../../../data/cloud";
import { saveCoreConfig } from "../../../data/core";
import type { PolymerChangedEvent } from "../../../polymer-types";
import type { HomeAssistant } from "../../../types";
import { isIPAddress } from "../../../common/string/is_ip_address";
@customElement("ha-config-url-form")
class ConfigUrlForm extends LitElement {
@@ -20,18 +33,48 @@ class ConfigUrlForm extends LitElement {
@state() private _internal_url?: string;
@state() private _cloudStatus?: CloudStatus | null;
@state() private _showCustomExternalUrl = false;
@state() private _showCustomInternalUrl = false;
protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes(
this.hass.config.config_source
);
const disabled = this._working || !canEdit;
if (!this.hass.userData?.showAdvanced) {
if (!this.hass.userData?.showAdvanced || this._cloudStatus === undefined) {
return html``;
}
const internalUrl = this._internalUrlValue;
const externalUrl = this._externalUrlValue;
let hasCloud: boolean;
let remoteEnabled: boolean;
let httpUseHttps: boolean;
if (this._cloudStatus === null) {
hasCloud = false;
remoteEnabled = false;
httpUseHttps = false;
} else {
httpUseHttps = this._cloudStatus.http_use_ssl;
if (this._cloudStatus.logged_in) {
hasCloud = true;
remoteEnabled =
this._cloudStatus.active_subscription &&
this._cloudStatus.prefs.remote_enabled;
} else {
hasCloud = false;
remoteEnabled = false;
}
}
return html`
<ha-card>
<ha-card .header=${this.hass.localize("ui.panel.config.url.caption")}>
<div class="card-content">
${!canEdit
? html`
@@ -43,46 +86,147 @@ class ConfigUrlForm extends LitElement {
`
: ""}
${this._error ? html`<div class="error">${this._error}</div>` : ""}
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.external_url"
)}
</div>
<paper-input
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.external_url"
)}
name="external_url"
type="url"
.disabled=${disabled}
.value=${this._externalUrlValue}
@value-changed=${this._handleChange}
>
</paper-input>
<div class="description">
${this.hass.localize("ui.panel.config.url.description")}
</div>
${hasCloud
? html`
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.url.external_url_label"
)}
</div>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.url.external_use_ha_cloud"
)}
>
<ha-switch
.disabled=${disabled}
.checked=${externalUrl === null}
@change=${this._toggleCloud}
></ha-switch>
</ha-formfield>
</div>
`
: ""}
${!this._showCustomExternalUrl
? ""
: html`
<div class="row">
<div class="flex">
${hasCloud
? ""
: this.hass.localize(
"ui.panel.config.url.external_url_label"
)}
</div>
<ha-textfield
class="flex"
name="external_url"
type="url"
.disabled=${disabled}
.value=${externalUrl || ""}
@change=${this._handleChange}
placeholder="https://example.duckdns.org:8123"
>
</ha-textfield>
</div>
`}
${hasCloud || !isComponentLoaded(this.hass, "cloud")
? ""
: html`
<div class="row">
<div class="flex"></div>
<a href="/config/cloud"
>${this.hass.localize(
"ui.panel.config.url.external_get_ha_cloud"
)}</a
>
</div>
`}
${!this._showCustomExternalUrl && hasCloud
? html`
${remoteEnabled
? ""
: html`
<ha-alert alert-type="error">
${this.hass.localize(
"ui.panel.config.url.ha_cloud_remote_not_enabled"
)}
<a href="/config/cloud" slot="action"
><mwc-button
.label=${this.hass.localize(
"ui.panel.config.url.enable_remote"
)}
></mwc-button
></a>
</ha-alert>
`}
`
: ""}
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.internal_url"
)}
${this.hass.localize("ui.panel.config.url.internal_url_label")}
</div>
<paper-input
class="flex"
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.internal_url"
"ui.panel.config.url.internal_url_automatic"
)}
name="internal_url"
type="url"
.disabled=${disabled}
.value=${this._internalUrlValue}
@value-changed=${this._handleChange}
>
</paper-input>
<ha-switch
.checked=${internalUrl === null}
@change=${this._toggleInternalAutomatic}
></ha-switch>
</ha-formfield>
</div>
${!this._showCustomInternalUrl
? ""
: html`
<div class="row">
<div class="flex"></div>
<ha-textfield
class="flex"
name="internal_url"
type="url"
placeholder="http://<some IP address>:8123"
.disabled=${disabled}
.value=${internalUrl || ""}
@change=${this._handleChange}
>
</ha-textfield>
</div>
`}
${
// If the user has configured a cert, show an error if
httpUseHttps && // there is no internal url configured
(!internalUrl ||
// the internal url does not start with https
!internalUrl.startsWith("https://") ||
// the internal url points at an IP address
isIPAddress(new URL(internalUrl).hostname))
? html`
<ha-alert
.alertType=${this._showCustomInternalUrl
? "info"
: "warning"}
.title=${this.hass.localize(
"ui.panel.config.url.intenral_url_https_error_title"
)}
>
${this.hass.localize(
"ui.panel.config.url.internal_url_https_error_description"
)}
</ha-alert>
`
: ""
}
</div>
<div class="card-actions">
<mwc-button @click=${this._save} .disabled=${disabled}>
@@ -95,6 +239,24 @@ class ConfigUrlForm extends LitElement {
`;
}
protected override firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._showCustomInternalUrl = this._internalUrlValue !== null;
if (isComponentLoaded(this.hass, "cloud")) {
fetchCloudStatus(this.hass).then((cloudStatus) => {
if (cloudStatus.logged_in) {
this._cloudStatus = cloudStatus;
this._showCustomExternalUrl = this._externalUrlValue !== null;
}
});
} else {
this._cloudStatus = null;
this._showCustomExternalUrl = true;
}
}
private get _internalUrlValue() {
return this._internal_url !== undefined
? this._internal_url
@@ -107,9 +269,17 @@ class ConfigUrlForm extends LitElement {
: this.hass.config.external_url;
}
private _toggleCloud(ev) {
this._showCustomExternalUrl = !ev.currentTarget.checked;
}
private _toggleInternalAutomatic(ev) {
this._showCustomInternalUrl = !ev.currentTarget.checked;
}
private _handleChange(ev: PolymerChangedEvent<string>) {
const target = ev.currentTarget as PaperInputElement;
this[`_${target.name}`] = target.value;
const target = ev.currentTarget as HaTextField;
this[`_${target.name}`] = target.value || null;
}
private async _save() {
@@ -117,8 +287,12 @@ class ConfigUrlForm extends LitElement {
this._error = undefined;
try {
await saveCoreConfig(this.hass, {
external_url: this._external_url || null,
internal_url: this._internal_url || null,
external_url: this._showCustomExternalUrl
? this._external_url || null
: null,
internal_url: this._showCustomInternalUrl
? this._internal_url || null
: null,
});
} catch (err: any) {
this._error = err.message || err;
@@ -129,11 +303,15 @@ class ConfigUrlForm extends LitElement {
static get styles(): CSSResultGroup {
return css`
.description {
margin-bottom: 1em;
}
.row {
display: flex;
flex-direction: row;
margin: 0 -8px;
align-items: center;
padding: 8px 0;
}
.secondary {
@@ -154,6 +332,10 @@ class ConfigUrlForm extends LitElement {
.card-actions {
text-align: right;
}
a {
color: var(--primary-color);
}
`;
}
}

View File

@@ -40,7 +40,7 @@ export class HaDeviceEntitiesCard extends LitElement {
@property() public entities!: EntityRegistryStateEntry[];
@property() public showDisabled = false;
@property() public showHidden = false;
@state() private _extDisabledEntityEntries?: Record<
string,
@@ -60,77 +60,77 @@ export class HaDeviceEntitiesCard extends LitElement {
}
protected render(): TemplateResult {
const disabledEntities: EntityRegistryStateEntry[] = [];
if (!this.entities.length) {
return html`
<ha-card .header=${this.header}>
<div class="empty card-content">
${this.hass.localize("ui.panel.config.devices.entities.none")}
</div>
</ha-card>
`;
}
const shownEntities: EntityRegistryStateEntry[] = [];
const hiddenEntities: EntityRegistryStateEntry[] = [];
this._entityRows = [];
this.entities.forEach((entry) => {
if (entry.disabled_by || entry.hidden_by) {
if (this._extDisabledEntityEntries) {
hiddenEntities.push(
this._extDisabledEntityEntries[entry.entity_id] || entry
);
} else {
hiddenEntities.push(entry);
}
} else {
shownEntities.push(entry);
}
});
return html`
<ha-card .header=${this.header}>
${this.entities.length
? html`
<div id="entities" @hass-more-info=${this._overrideMoreInfo}>
${this.entities.map((entry: EntityRegistryStateEntry) => {
if (entry.disabled_by) {
if (this._extDisabledEntityEntries) {
disabledEntities.push(
this._extDisabledEntityEntries[entry.entity_id] || entry
);
} else {
disabledEntities.push(entry);
}
return "";
}
return this.hass.states[entry.entity_id]
? this._renderEntity(entry)
: this._renderEntry(entry);
})}
</div>
${disabledEntities.length
? !this.showDisabled
? html`
<button
class="show-more"
@click=${this._toggleShowDisabled}
>
${this.hass.localize(
"ui.panel.config.devices.entities.disabled_entities",
"count",
disabledEntities.length
)}
</button>
`
: html`
${disabledEntities.map((entry) =>
this._renderEntry(entry)
)}
<button
class="show-more"
@click=${this._toggleShowDisabled}
>
${this.hass.localize(
"ui.panel.config.devices.entities.hide_disabled"
)}
</button>
`
: ""}
<div class="card-actions">
<mwc-button @click=${this._addToLovelaceView}>
<div id="entities" @hass-more-info=${this._overrideMoreInfo}>
${shownEntities.map((entry) =>
this.hass.states[entry.entity_id]
? this._renderEntity(entry)
: this._renderEntry(entry)
)}
</div>
${hiddenEntities.length
? !this.showHidden
? html`
<button class="show-more" @click=${this._toggleShowHidden}>
${this.hass.localize(
"ui.panel.config.devices.entities.add_entities_lovelace"
"ui.panel.config.devices.entities.hidden_entities",
"count",
hiddenEntities.length
)}
</mwc-button>
</div>
`
: html`
<div class="empty card-content">
${this.hass.localize("ui.panel.config.devices.entities.none")}
</div>
`}
</button>
`
: html`
${hiddenEntities.map((entry) => this._renderEntry(entry))}
<button class="show-more" @click=${this._toggleShowHidden}>
${this.hass.localize(
"ui.panel.config.devices.entities.hide_disabled"
)}
</button>
`
: ""}
<div class="card-actions">
<mwc-button @click=${this._addToLovelaceView}>
${this.hass.localize(
"ui.panel.config.devices.entities.add_entities_lovelace"
)}
</mwc-button>
</div>
</ha-card>
`;
}
private _toggleShowDisabled() {
this.showDisabled = !this.showDisabled;
if (!this.showDisabled || this._extDisabledEntityEntries !== undefined) {
private _toggleShowHidden() {
this.showHidden = !this.showHidden;
if (!this.showHidden || this._extDisabledEntityEntries !== undefined) {
return;
}
this._extDisabledEntityEntries = {};

View File

@@ -1,84 +0,0 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { navigate } from "../../../../../../common/navigate";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import {
getIdentifiersFromDevice,
OZWNodeIdentifiers,
} from "../../../../../../data/ozw";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node";
@customElement("ha-device-actions-ozw")
export class HaDeviceActionsOzw extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@property()
private node_id = 0;
@property()
private ozw_instance = 1;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const identifiers: OZWNodeIdentifiers | undefined =
getIdentifiersFromDevice(this.device);
if (!identifiers) {
return;
}
this.ozw_instance = identifiers.ozw_instance;
this.node_id = identifiers.node_id;
}
}
protected render(): TemplateResult {
if (!this.ozw_instance || !this.node_id) {
return html``;
}
return html`
<mwc-button @click=${this._nodeDetailsClicked}>
${this.hass.localize("ui.panel.config.ozw.node.button")}
</mwc-button>
<mwc-button @click=${this._refreshNodeClicked}>
${this.hass.localize("ui.panel.config.ozw.refresh_node.button")}
</mwc-button>
`;
}
private async _refreshNodeClicked() {
showOZWRefreshNodeDialog(this, {
node_id: this.node_id,
ozw_instance: this.ozw_instance,
});
}
private async _nodeDetailsClicked() {
navigate(
`/config/ozw/network/${this.ozw_instance}/node/${this.node_id}/dashboard`
);
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: flex;
flex-direction: column;
align-items: flex-start;
}
`,
];
}
}

View File

@@ -1,99 +0,0 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import {
fetchOZWNodeStatus,
getIdentifiersFromDevice,
OZWDevice,
OZWNodeIdentifiers,
} from "../../../../../../data/ozw";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
@customElement("ha-device-info-ozw")
export class HaDeviceInfoOzw extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@property()
private node_id = 0;
@property()
private ozw_instance = 1;
@state() private _ozwDevice?: OZWDevice;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const identifiers: OZWNodeIdentifiers | undefined =
getIdentifiersFromDevice(this.device);
if (!identifiers) {
return;
}
this.ozw_instance = identifiers.ozw_instance;
this.node_id = identifiers.node_id;
this._fetchNodeDetails();
}
}
protected async _fetchNodeDetails() {
this._ozwDevice = await fetchOZWNodeStatus(
this.hass,
this.ozw_instance,
this.node_id
);
}
protected render(): TemplateResult {
if (!this._ozwDevice) {
return html``;
}
return html`
<h4>
${this.hass.localize("ui.panel.config.ozw.device_info.zwave_info")}
</h4>
<div>
${this.hass.localize("ui.panel.config.ozw.common.node_id")}:
${this._ozwDevice.node_id}
</div>
<div>
${this.hass.localize("ui.panel.config.ozw.device_info.stage")}:
${this._ozwDevice.node_query_stage}
</div>
<div>
${this.hass.localize("ui.panel.config.ozw.common.ozw_instance")}:
${this._ozwDevice.ozw_instance}
</div>
<div>
${this.hass.localize("ui.panel.config.ozw.device_info.node_failed")}:
${this._ozwDevice.is_failed
? this.hass.localize("ui.common.yes")
: this.hass.localize("ui.common.no")}
</div>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
h4 {
margin-bottom: 4px;
}
div {
word-break: break-all;
margin-top: 2px;
}
`,
];
}
}

View File

@@ -27,7 +27,7 @@ import { HomeAssistant } from "../../../../../../types";
export class HaDeviceInfoZWaveJS extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@property({ attribute: false }) public device!: DeviceRegistryEntry;
@state() private _entryId?: string;
@@ -173,3 +173,9 @@ export class HaDeviceInfoZWaveJS extends LitElement {
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-info-zwave_js": HaDeviceInfoZWaveJS;
}
}

View File

@@ -557,7 +557,7 @@ export class HaConfigDevicePage extends LitElement {
)}
.deviceName=${deviceName}
.entities=${entitiesByCategory[category]}
.showDisabled=${device.disabled_by !== null}
.showHidden=${device.disabled_by !== null}
>
</ha-device-entities-card>
`
@@ -902,22 +902,6 @@ export class HaConfigDevicePage extends LitElement {
></ha-device-actions-mqtt>
`);
}
if (domains.includes("ozw")) {
import("./device-detail/integration-elements/ozw/ha-device-actions-ozw");
import("./device-detail/integration-elements/ozw/ha-device-info-ozw");
deviceInfo.push(html`
<ha-device-info-ozw
.hass=${this.hass}
.device=${device}
></ha-device-info-ozw>
`);
deviceActions.push(html`
<ha-device-actions-ozw
.hass=${this.hass}
.device=${device}
></ha-device-actions-ozw>
`);
}
if (domains.includes("zha")) {
import("./device-detail/integration-elements/zha/ha-device-actions-zha");
import("./device-detail/integration-elements/zha/ha-device-info-zha");

View File

@@ -19,6 +19,7 @@ import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-radio";
import "../../../../components/ha-formfield";
import "../../../../components/ha-textfield";
import type { HaRadio } from "../../../../components/ha-radio";
@customElement("dialog-energy-gas-settings")
@@ -188,20 +189,19 @@ export class DialogEnergyGasSettings
></ha-radio>
</ha-formfield>
${this._costs === "number"
? html`<paper-input
? html`<ha-textfield
.label=${this.hass.localize(
`ui.panel.config.energy.gas.dialog.cost_number_input`,
{ unit }
)}
no-label-float
class="price-options"
step=".01"
type="number"
.value=${this._source.number_energy_price}
@value-changed=${this._numberPriceChanged}
@change=${this._numberPriceChanged}
.suffix=${`${this.hass.config.currency}/${unit}`}
>
<span slot="suffix">${this.hass.config.currency}/${unit}</span>
</paper-input>`
</ha-textfield>`
: ""}
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
@@ -223,10 +223,10 @@ export class DialogEnergyGasSettings
this._costs = input.value as any;
}
private _numberPriceChanged(ev: CustomEvent) {
private _numberPriceChanged(ev) {
this._source = {
...this._source!,
number_energy_price: Number(ev.detail.value),
number_energy_price: Number(ev.target.value),
entity_energy_price: null,
stat_cost: null,
};
@@ -295,13 +295,10 @@ export class DialogEnergyGasSettings
ha-formfield {
display: block;
}
ha-statistic-picker {
width: 100%;
}
.price-options {
display: block;
padding-left: 52px;
margin-top: -16px;
margin-top: -8px;
}
`,
];

View File

@@ -190,24 +190,21 @@ export class DialogEnergyGridFlowSettings
></ha-radio>
</ha-formfield>
${this._costs === "number"
? html`<paper-input
? html`<ha-textfield
.label=${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_number_input`
)}
no-label-float
class="price-options"
step=".01"
type="number"
.value=${this._source.number_energy_price}
@value-changed=${this._numberPriceChanged}
.suffix=${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_number_suffix`,
{ currency: this.hass.config.currency }
)}
@change=${this._numberPriceChanged}
>
<span slot="suffix"
>${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_number_suffix`,
{ currency: this.hass.config.currency }
)}</span
>
</paper-input>`
</ha-textfield>`
: ""}
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
@@ -243,7 +240,7 @@ export class DialogEnergyGridFlowSettings
this._costStat = null;
this._source = {
...this._source!,
number_energy_price: Number(ev.detail.value),
number_energy_price: Number((ev.target as any).value),
entity_energy_price: null,
};
}
@@ -302,13 +299,10 @@ export class DialogEnergyGridFlowSettings
ha-formfield {
display: block;
}
ha-statistic-picker {
width: 100%;
}
.price-options {
display: block;
padding-left: 52px;
margin-top: -16px;
margin-top: -8px;
}
`,
];

View File

@@ -1,3 +1,5 @@
import "../../../components/ha-expansion-panel";
import "@material/mwc-formfield/mwc-formfield";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -5,7 +7,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-area-picker";
import "../../../components/ha-switch";
import "../../../components/ha-textfield";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-radio";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
@@ -33,6 +35,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@state() private _disabledBy!: string | null;
@state() private _hiddenBy!: string | null;
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
@state() private _device?: DeviceRegistryEntry;
@@ -51,6 +55,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
) {
params.disabled_by = this._disabledBy;
}
if (
this.entry.hidden_by !== this._hiddenBy &&
(this._hiddenBy === null || this._hiddenBy === "user")
) {
params.hidden_by = this._hiddenBy;
}
try {
const result = await updateEntityRegistryEntry(
this.hass!,
@@ -101,6 +111,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
this._origEntityId = this.entry.entity_id;
this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by;
this._hiddenBy = this.entry.hidden_by;
this._areaId = this.entry.area_id;
this._device =
this.entry.device_id && this._deviceLookup
@@ -138,37 +149,95 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
.placeholder=${this._device?.area_id}
@value-changed=${this._areaPicked}
></ha-area-picker>
<div class="row">
<ha-switch
.checked=${!this._disabledBy}
@change=${this._disabledByChanged}
>
</ha-switch>
<div>
<div>
${this.hass.localize(
<ha-expansion-panel
.header=${this.hass.localize(
"ui.dialogs.entity_registry.editor.advanced"
)}
outlined
>
<div class="label">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.entity_status"
)}:
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
</div>
<div class="row">
<mwc-formfield
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_label"
)}
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_description"
>
<ha-radio
name="hiddendisabled"
value="enabled"
.checked=${!this._hiddenBy && !this._disabledBy}
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
this._device?.disabled_by ||
(this._disabledBy && this._disabledBy !== "user")}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
<mwc-formfield
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.hidden_label"
)}
<br />${this.hass.localize(
"ui.dialogs.entity_registry.editor.note"
>
<ha-radio
name="hiddendisabled"
value="hidden"
.checked=${this._hiddenBy !== null}
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
Boolean(this._device?.disabled_by) ||
(this._disabledBy && this._disabledBy !== "user")}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
<mwc-formfield
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.disabled_label"
)}
</div>
>
<ha-radio
name="hiddendisabled"
value="disabled"
.checked=${this._disabledBy !== null}
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
Boolean(this._device?.disabled_by) ||
(this._disabledBy && this._disabledBy !== "user")}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
</div>
</div>
${this._disabledBy !== null
? html`
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_description"
)}
</div>
`
: this._hiddenBy !== null
? html`
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.hidden_description"
)}
</div>
`
: ""}
</ha-expansion-panel>
`;
}
@@ -180,8 +249,21 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
this._entityId = ev.target.value;
}
private _disabledByChanged(ev: Event): void {
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
private _viewStatusChanged(ev: CustomEvent): void {
switch ((ev.target as any).value) {
case "enabled":
this._disabledBy = null;
this._hiddenBy = null;
break;
case "disabled":
this._disabledBy = "user";
this._hiddenBy = null;
break;
case "hidden":
this._hiddenBy = "user";
this._disabledBy = null;
break;
}
}
static get styles() {
@@ -202,6 +284,12 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
display: block;
margin-bottom: 8px;
}
ha-expansion-panel {
margin-top: 8px;
}
.label {
margin-top: 16px;
}
`;
}
}

View File

@@ -1,3 +1,5 @@
import "@material/mwc-formfield/mwc-formfield";
import "../../../components/ha-radio";
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
@@ -11,6 +13,7 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import { domainIcon } from "../../../common/entity/domain_icon";
import "../../../components/ha-alert";
@@ -19,7 +22,6 @@ import "../../../components/ha-expansion-panel";
import "../../../components/ha-icon-picker";
import "../../../components/ha-select";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-textfield";
import {
DeviceRegistryEntry,
@@ -42,7 +44,18 @@ import type { HomeAssistant } from "../../../types";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
const OVERRIDE_DEVICE_CLASSES = {
cover: ["window", "door", "garage", "gate"],
cover: [
"awning",
"blind",
"curtain",
"damper",
"door",
"garage",
"gate",
"shade",
"shutter",
"window",
],
binary_sensor: ["window", "door", "garage_door", "opening"],
};
@@ -64,6 +77,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _disabledBy!: string | null;
@state() private _hiddenBy!: string | null;
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
@state() private _device?: DeviceRegistryEntry;
@@ -100,6 +115,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._areaId = this.entry.area_id;
this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by;
this._hiddenBy = this.entry.hidden_by;
this._device =
this.entry.device_id && this._deviceLookup
? this._deviceLookup[this.entry.device_id]
@@ -166,7 +182,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
"ui.dialogs.entity_registry.editor.device_class"
)}
.value=${this._deviceClass}
naturalMenuWidth
fixedMenuPosition
@selected=${this._deviceClassChanged}
@closed=${stopPropagation}
>
${OVERRIDE_DEVICE_CLASSES[domain].map(
(deviceClass: string) => html`
@@ -196,75 +215,126 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@value-changed=${this._areaPicked}
></ha-area-picker>`
: ""}
<div class="row">
<ha-switch
.checked=${!this._disabledBy}
.disabled=${this._device?.disabled_by}
@change=${this._disabledByChanged}
>
</ha-switch>
<div>
<div>
${this.hass.localize(
<ha-expansion-panel
.header=${this.hass.localize(
"ui.dialogs.entity_registry.editor.advanced"
)}
outlined
>
<div class="label">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.entity_status"
)}:
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
</div>
<div class="row">
<mwc-formfield
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_label"
)}
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_description"
)}
<br />${this.hass.localize(
"ui.dialogs.entity_registry.editor.note"
)}
</div>
</div>
</div>
${this.entry.device_id
? html`<ha-expansion-panel
.header=${this.hass.localize(
"ui.dialogs.entity_registry.editor.advanced"
)}
outlined
>
<p>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.area_note"
)}
</p>
${this._areaId
? html`<mwc-button @click=${this._clearArea}
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.follow_device_area"
)}</mwc-button
>`
: this._device
? html`<mwc-button @click=${this._openDeviceSettings}
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.change_device_area"
)}</mwc-button
>`
: ""}
<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
.placeholder=${this._device?.area_id}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.area"
)}
@value-changed=${this._areaPicked}
></ha-area-picker
></ha-expansion-panel>`
: ""}
<ha-radio
name="hiddendisabled"
value="enabled"
.checked=${!this._hiddenBy && !this._disabledBy}
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
this._device?.disabled_by ||
(this._disabledBy && this._disabledBy !== "user")}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
<mwc-formfield
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.hidden_label"
)}
>
<ha-radio
name="hiddendisabled"
value="hidden"
.checked=${this._hiddenBy !== null}
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
Boolean(this._device?.disabled_by) ||
(this._disabledBy && this._disabledBy !== "user")}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
<mwc-formfield
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.disabled_label"
)}
>
<ha-radio
name="hiddendisabled"
value="disabled"
.checked=${this._disabledBy !== null}
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
Boolean(this._device?.disabled_by) ||
(this._disabledBy && this._disabledBy !== "user")}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
</div>
${this._disabledBy !== null
? html`
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_description"
)}
</div>
`
: this._hiddenBy !== null
? html`
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.hidden_description"
)}
</div>
`
: ""}
${this.entry.device_id
? html`
<div class="label">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.change_area"
)}:
</div>
<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
.placeholder=${this._device?.area_id}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.area"
)}
@value-changed=${this._areaPicked}
></ha-area-picker>
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.area_note"
)}
${this._device
? html`
<button class="link" @click=${this._openDeviceSettings}>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.change_device_area"
)}
</button>
`
: ""}
</div>
`
: ""}
</ha-expansion-panel>
</div>
<div class="buttons">
<mwc-button
@@ -310,9 +380,21 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._areaId = ev.detail.value;
}
private _clearArea() {
this._error = undefined;
this._areaId = null;
private _viewStatusChanged(ev: CustomEvent): void {
switch ((ev.target as any).value) {
case "enabled":
this._disabledBy = null;
this._hiddenBy = null;
break;
case "disabled":
this._disabledBy = "user";
this._hiddenBy = null;
break;
case "hidden":
this._hiddenBy = "user";
this._disabledBy = null;
break;
}
}
private _openDeviceSettings() {
@@ -339,6 +421,12 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
) {
params.disabled_by = this._disabledBy;
}
if (
this.entry.hidden_by !== this._hiddenBy &&
(this._hiddenBy === null || this._hiddenBy === "user")
) {
params.hidden_by = this._hiddenBy;
}
try {
const result = await updateEntityRegistryEntry(
this.hass!,
@@ -390,10 +478,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
}
private _disabledByChanged(ev: Event): void {
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -422,6 +506,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
ha-select {
width: 100%;
margin: 8px 0;
}
ha-switch {
margin-right: 16px;
@@ -430,14 +515,22 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
display: block;
margin: 8px 0;
}
ha-area-picker {
margin: 8px 0;
display: block;
}
.row {
margin: 8px 0;
color: var(--primary-text-color);
display: flex;
align-items: center;
}
p {
.label {
margin-top: 16px;
}
.secondary {
margin: 8px 0;
width: 340px;
}
`,
];

View File

@@ -3,6 +3,7 @@ import {
mdiAlertCircle,
mdiCancel,
mdiDelete,
mdiEyeOff,
mdiFilterVariant,
mdiPencilOff,
mdiPlus,
@@ -101,6 +102,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@state() private _showDisabled = false;
@state() private _showHidden = false;
@state() private _showUnavailable = true;
@state() private _showReadOnly = true;
@@ -249,7 +252,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
filterable: true,
width: "68px",
template: (_status, entity: EntityRow) =>
entity.unavailable || entity.disabled_by || entity.readonly
entity.unavailable ||
entity.disabled_by ||
entity.hidden_by ||
entity.readonly
? html`
<div
tabindex="0"
@@ -265,6 +271,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
? mdiAlertCircle
: entity.disabled_by
? mdiCancel
: entity.hidden_by
? mdiEyeOff
: mdiPencilOff}
></ha-svg-icon>
<paper-tooltip animation-delay="0" position="left">
@@ -280,6 +288,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
? this.hass.localize(
"ui.panel.config.entities.picker.status.disabled"
)
: entity.hidden_by
? this.hass.localize(
"ui.panel.config.entities.picker.status.hidden"
)
: this.hass.localize(
"ui.panel.config.entities.picker.status.readonly"
)}
@@ -301,6 +313,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
showDisabled: boolean,
showUnavailable: boolean,
showReadOnly: boolean,
showHidden: boolean,
entries?: ConfigEntry[]
) => {
const result: EntityRow[] = [];
@@ -362,6 +375,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
);
}
if (!showHidden) {
filteredEntities = filteredEntities.filter(
(entity) => !entity.hidden_by
);
}
for (const entry of filteredEntities) {
const entity = this.hass.states[entry.entity_id];
const unavailable = entity?.state === UNAVAILABLE;
@@ -465,6 +484,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._showDisabled,
this._showUnavailable,
this._showReadOnly,
this._showHidden,
this._entries
);
@@ -533,6 +553,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
"ui.panel.config.entities.picker.disable_selected.button"
)}</mwc-button
>
<mwc-button @click=${this._hideSelected}
>${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}</mwc-button
>
<mwc-button @click=${this._removeSelected} class="warning"
>${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
@@ -562,6 +587,17 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
"ui.panel.config.entities.picker.disable_selected.button"
)}
</paper-tooltip>
<ha-icon-button
id="hide-btn"
@click=${this._hideSelected}
.path=${mdiCancel}
.label=${this.hass.localize("ui.common.hide")}
></ha-icon-button>
<paper-tooltip animation-delay="0" for="hide-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}
</paper-tooltip>
<ha-icon-button
class="warning"
id="remove-btn"
@@ -603,6 +639,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
"ui.panel.config.entities.picker.filter.show_disabled"
)}
</ha-check-list-item>
<ha-check-list-item
@request-selected=${this._showHiddenChanged}
.selected=${this._showHidden}
left
>
${this.hass!.localize(
"ui.panel.config.entities.picker.filter.show_hidden"
)}
</ha-check-list-item>
<ha-check-list-item
@request-selected=${this._showRestoredChanged}
graphic="control"
@@ -671,6 +716,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
entity_id: entityId,
platform: computeDomain(entityId),
disabled_by: null,
hidden_by: null,
area_id: null,
config_entry_id: null,
device_id: null,
@@ -693,6 +739,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._showDisabled = ev.detail.selected;
}
private _showHiddenChanged(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "property") {
return;
}
this._showHidden = ev.detail.selected;
}
private _showRestoredChanged(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "property") {
return;
@@ -791,6 +844,29 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
});
}
private _hideSelected() {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.confirm_title",
"number",
this._selectedEntities.length
),
text: this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.confirm_text"
),
confirmText: this.hass.localize("ui.common.hide"),
dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => {
this._selectedEntities.forEach((entity) =>
updateEntityRegistryEntry(this.hass, entity, {
hidden_by: "user",
})
);
this._clearSelection();
},
});
}
private _removeSelected() {
const removeableEntities = this._selectedEntities.filter((entity) => {
const stateObj = this.hass.states[entity];

View File

@@ -376,21 +376,11 @@ class HaPanelConfig extends HassRouterPage {
"./integrations/integration-panels/zha/zha-config-dashboard-router"
),
},
zwave: {
tag: "zwave-config-router",
load: () =>
import("./integrations/integration-panels/zwave/zwave-config-router"),
},
mqtt: {
tag: "mqtt-config-panel",
load: () =>
import("./integrations/integration-panels/mqtt/mqtt-config-panel"),
},
ozw: {
tag: "ozw-config-router",
load: () =>
import("./integrations/integration-panels/ozw/ozw-config-router"),
},
zwave_js: {
tag: "zwave_js-config-router",
load: () =>

View File

@@ -85,7 +85,7 @@ class HaInputSelectForm extends LitElement {
${this._options.length
? this._options.map(
(option, index) => html`
<mwc-list-item class="option" hasMeta noninteractive>
<mwc-list-item class="option" hasMeta>
${option}
<ha-icon-button
slot="meta"

View File

@@ -30,6 +30,7 @@ import "../../../components/ha-check-list-item";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
import {
getConfigFlowHandlers,
getConfigFlowInProgressCollection,
localizeConfigFlowTitle,
subscribeConfigFlowInProgress,
@@ -51,7 +52,10 @@ import {
} from "../../../data/integration";
import { scanUSBDevices } from "../../../data/usb";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
@@ -652,6 +656,19 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
if (!domain) {
return;
}
const handlers = await getConfigFlowHandlers(this.hass);
if (!handlers.includes(domain)) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.no_config_flow"
),
});
return;
}
const localize = await localizePromise;
if (
!(await showConfirmationDialog(this, {

View File

@@ -55,8 +55,6 @@ const integrationsWithPanel = {
hassio: "/hassio/dashboard",
mqtt: "/config/mqtt",
zha: "/config/zha/dashboard",
ozw: "/config/ozw/dashboard",
zwave: "/config/zwave",
zwave_js: "/config/zwave_js/dashboard",
};

View File

@@ -1,269 +0,0 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-circular-progress";
import "../../../../../components/ha-code-editor";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import {
fetchOZWNodeMetadata,
nodeQueryStages,
OZWDevice,
OZWDeviceMetaData,
} from "../../../../../data/ozw";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { OZWRefreshNodeDialogParams } from "./show-dialog-ozw-refresh-node";
@customElement("dialog-ozw-refresh-node")
class DialogOZWRefreshNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _node_id?: number;
@state() private _ozw_instance = 1;
@state() private _nodeMetaData?: OZWDeviceMetaData;
@state() private _node?: OZWDevice;
@state() private _active = false;
@state() private _complete = false;
private _refreshDevicesTimeoutHandle?: number;
private _subscribed?: Promise<() => Promise<void>>;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._unsubscribe();
}
protected updated(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("node_id")) {
this._fetchData();
}
}
private async _fetchData() {
if (!this._node_id) {
return;
}
const metaDataResponse = await fetchOZWNodeMetadata(
this.hass,
this._ozw_instance,
this._node_id
);
this._nodeMetaData = metaDataResponse.metadata;
}
public async showDialog(params: OZWRefreshNodeDialogParams): Promise<void> {
this._node_id = params.node_id;
this._ozw_instance = params.ozw_instance;
this._fetchData();
}
protected render(): TemplateResult {
if (!this._node_id) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this._close}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.ozw.refresh_node.title")
)}
>
${this._complete
? html`
<p>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.complete"
)}
</p>
<mwc-button slot="primaryAction" @click=${this._close}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: html`
${this._active
? html`
<div class="flex-container">
<ha-circular-progress active></ha-circular-progress>
<div>
<p>
<b>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.refreshing_description"
)}
</b>
</p>
${this._node
? html`
<p>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.node_status"
)}:
${this._node.node_query_stage}
(${this.hass.localize(
"ui.panel.config.ozw.refresh_node.step"
)}
${nodeQueryStages.indexOf(
this._node.node_query_stage
) + 1}/17)
</p>
<p>
<em>
${this.hass.localize(
"ui.panel.config.ozw.node_query_stages." +
this._node.node_query_stage.toLowerCase()
)}</em
>
</p>
`
: ``}
</div>
</div>
`
: html`
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.description"
)}
<p>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.battery_note"
)}
</p>
`}
${this._nodeMetaData?.WakeupHelp !== ""
? html`
<b>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.wakeup_header"
)}
${this._nodeMetaData!.Name}
</b>
<blockquote>
${this._nodeMetaData!.WakeupHelp}
<br />
<em>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.wakeup_instructions_source"
)}
</em>
</blockquote>
`
: ""}
${!this._active
? html`
<mwc-button
slot="primaryAction"
@click=${this._startRefresh}
>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.start_refresh_button"
)}
</mwc-button>
`
: html``}
`}
</ha-dialog>
`;
}
private _startRefresh(): void {
this._subscribe();
}
private _handleMessage(message: any): void {
if (message.type === "node_updated") {
this._node = message;
if (message.node_query_stage === "Complete") {
this._unsubscribe();
this._complete = true;
}
}
}
private _unsubscribe(): void {
this._active = false;
if (this._refreshDevicesTimeoutHandle) {
clearTimeout(this._refreshDevicesTimeoutHandle);
}
if (this._subscribed) {
this._subscribed.then((unsub) => unsub());
this._subscribed = undefined;
}
}
private _subscribe(): void {
if (!this.hass) {
return;
}
this._active = true;
this._subscribed = this.hass.connection.subscribeMessage(
(message) => this._handleMessage(message),
{
type: "ozw/refresh_node_info",
node_id: this._node_id,
ozw_instance: this._ozw_instance,
}
);
this._refreshDevicesTimeoutHandle = window.setTimeout(
() => this._unsubscribe(),
120000
);
}
private _close(): void {
this._complete = false;
this._node_id = undefined;
this._node = undefined;
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
blockquote {
display: block;
background-color: #ddd;
padding: 8px;
margin: 8px 0;
font-size: 0.9em;
}
blockquote em {
font-size: 0.9em;
margin-top: 6px;
}
.flex-container {
display: flex;
align-items: center;
}
.flex-container ha-circular-progress {
margin-right: 20px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-ozw-refresh-node": DialogOZWRefreshNode;
}
}

View File

@@ -1,260 +0,0 @@
import "@material/mwc-button/mwc-button";
import { mdiCheckCircle, mdiCircle, mdiCloseCircle, mdiZWave } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import {
fetchOZWInstances,
networkOfflineStatuses,
networkOnlineStatuses,
networkStartingStatuses,
OZWInstance,
} from "../../../../../data/ozw";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-loading-screen";
import "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import "../../../../../components/ha-alert";
export const ozwTabs: PageNavigation[] = [];
@customElement("ozw-config-dashboard")
class OZWConfigDashboard extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@state() private _instances?: OZWInstance[];
protected firstUpdated() {
this._fetchData();
}
protected render(): TemplateResult {
if (!this._instances) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (this._instances.length === 0) {
return html`<hass-error-screen
.hass=${this.hass}
.error=${this.hass.localize(
"ui.panel.config.ozw.select_instance.none_found"
)}
></hass-error-screen>`;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwTabs}
back-path="/config/integrations"
>
<ha-alert
alert-type="warning"
title="This integration will stop working soon"
>
The OpenZWave integration is deprecated and will no longer receive any
updates. The technical dependencies will render this integration
unusable in the near future. We strongly advise you to migrate to the
new
<a
href="https://www.home-assistant.io/integrations/zwave_js"
target="_blank"
rel="noreferrer"
>Z-Wave JS integration</a
>.
<a
slot="action"
href="https://alerts.home-assistant.io/#ozw.markdown"
target="_blank"
rel="noreferrer"
>
<mwc-button>learn more</mwc-button>
</a>
</ha-alert>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.ozw.select_instance.header")}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.ozw.select_instance.introduction"
)}
</div>
${this._instances.length > 0
? html`
${this._instances.map((instance) => {
let status = "unknown";
let icon = mdiCircle;
if (networkOnlineStatuses.includes(instance.Status)) {
status = "online";
icon = mdiCheckCircle;
}
if (networkStartingStatuses.includes(instance.Status)) {
status = "starting";
}
if (networkOfflineStatuses.includes(instance.Status)) {
status = "offline";
icon = mdiCloseCircle;
}
return html`
<ha-card>
<a
href="/config/ozw/network/${instance.ozw_instance}"
role="option"
tabindex="-1"
>
<paper-icon-item>
<ha-svg-icon .path=${mdiZWave} slot="item-icon">
</ha-svg-icon>
<paper-item-body>
${this.hass.localize(
"ui.panel.config.ozw.common.instance"
)}
${instance.ozw_instance}
<div secondary>
<ha-svg-icon
.path=${icon}
class="network-status-icon ${status}"
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.ozw.network_status." + status
)}
-
${this.hass.localize(
"ui.panel.config.ozw.network_status.details." +
instance.Status.toLowerCase()
)}<br />
${this.hass.localize(
"ui.panel.config.ozw.common.controller"
)}
: ${instance.getControllerPath}<br />
OZWDaemon ${instance.OZWDaemon_Version} (OpenZWave
${instance.OpenZWave_Version})
</div>
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-icon-item>
</a>
</ha-card>
`;
})}
`
: ""}
</ha-config-section>
</hass-tabs-subpage>
`;
}
private async _fetchData() {
this._instances = await fetchOZWInstances(this.hass!);
if (this._instances.length === 1) {
navigate(`/config/ozw/network/${this._instances[0].ozw_instance}`, {
replace: true,
});
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-card:last-child {
margin-bottom: 24px;
}
ha-config-section {
margin-top: -12px;
}
:host([narrow]) ha-config-section {
margin-top: -20px;
}
ha-alert {
display: block;
margin: 16px;
}
ha-alert a {
text-decoration: none;
}
ha-card {
overflow: hidden;
}
ha-card a {
text-decoration: none;
color: var(--primary-text-color);
}
paper-item-body {
margin: 16px 0;
}
a {
text-decoration: none;
color: var(--primary-text-color);
position: relative;
display: block;
outline: 0;
}
ha-svg-icon.network-status-icon {
height: 14px;
width: 14px;
}
.online {
color: green;
}
.starting {
color: orange;
}
.offline {
color: red;
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
}
.iron-selected paper-item::before,
a:not(.iron-selected):focus::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: "";
transition: opacity 15ms linear;
will-change: opacity;
}
a:not(.iron-selected):focus::before {
background-color: currentColor;
opacity: var(--dark-divider-opacity);
}
.iron-selected paper-item:focus::before,
.iron-selected:focus paper-item::before {
opacity: 0.2;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-config-dashboard": OZWConfigDashboard;
}
}

View File

@@ -1,67 +0,0 @@
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
import { HomeAssistant, Route } from "../../../../../types";
export const computeTail = memoizeOne((route: Route) => {
const dividerPos = route.path.indexOf("/", 1);
return dividerPos === -1
? {
prefix: route.prefix + route.path,
path: "",
}
: {
prefix: route.prefix + route.path.substr(0, dividerPos),
path: route.path.substr(dividerPos),
};
});
@customElement("ozw-config-router")
class OZWConfigRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
routes: {
dashboard: {
tag: "ozw-config-dashboard",
load: () => import("./ozw-config-dashboard"),
},
network: {
tag: "ozw-network-router",
load: () => import("./ozw-network-router"),
},
},
};
protected updatePageEl(el): void {
el.route = this.routeTail;
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
if (this._currentPage === "network") {
const path = this.routeTail.path.split("/");
el.ozwInstance = path[1];
el.route = computeTail(this.routeTail);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-config-router": OZWConfigRouter;
}
}

View File

@@ -1,245 +0,0 @@
import "@material/mwc-button/mwc-button";
import { mdiCheckCircle, mdiCircle, mdiCloseCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import {
fetchOZWNetworkStatistics,
fetchOZWNetworkStatus,
networkOfflineStatuses,
networkOnlineStatuses,
networkStartingStatuses,
OZWInstance,
OZWNetworkStatistics,
} from "../../../../../data/ozw";
import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { ozwNetworkTabs } from "./ozw-network-router";
@customElement("ozw-network-dashboard")
class OZWNetworkDashboard extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@property() public ozwInstance?: number;
@state() private _network?: OZWInstance;
@state() private _statistics?: OZWNetworkStatistics;
@state() private _status = "unknown";
@state() private _icon = mdiCircle;
protected firstUpdated() {
if (!this.ozwInstance) {
navigate("/config/ozw/dashboard", { replace: true });
} else if (this.hass) {
this._fetchData();
}
}
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwNetworkTabs(this.ozwInstance!)}
>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.ozw.network.header")}
</div>
<div slot="introduction">
${this.hass.localize("ui.panel.config.ozw.network.introduction")}
</div>
${this._network
? html`
<ha-card class="content network-status">
<div class="card-content">
<div class="details">
<ha-svg-icon
.path=${this._icon}
class="network-status-icon ${classMap({
[this._status]: true,
})}"
slot="item-icon"
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.ozw.common.network"
)}
${this.hass.localize(
`ui.panel.config.ozw.network_status.${this._status}`
)}
<br />
<small>
${this.hass.localize(
`ui.panel.config.ozw.network_status.details.${this._network.Status.toLowerCase()}`
)}
</small>
</div>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.ozw.common.ozw_instance"
)}
${this._network.ozw_instance}
${this._statistics
? html`
&bull;
${this.hass.localize(
"ui.panel.config.ozw.network.node_count",
"count",
this._statistics.node_count
)}
`
: ``}
<br />
${this.hass.localize(
"ui.panel.config.ozw.common.controller"
)}:
${this._network.getControllerPath}<br />
OZWDaemon ${this._network.OZWDaemon_Version} (OpenZWave
${this._network.OpenZWave_Version})
</div>
</div>
<div class="card-actions">
${this._generateServiceButton("add_node")}
${this._generateServiceButton("remove_node")}
${this._generateServiceButton("cancel_command")}
</div>
</ha-card>
`
: ``}
</ha-config-section>
</hass-tabs-subpage>
`;
}
private async _fetchData() {
if (!this.ozwInstance) return;
this._network = await fetchOZWNetworkStatus(this.hass!, this.ozwInstance);
this._statistics = await fetchOZWNetworkStatistics(
this.hass!,
this.ozwInstance
);
if (networkOnlineStatuses.includes(this._network!.Status)) {
this._status = "online";
this._icon = mdiCheckCircle;
}
if (networkStartingStatuses.includes(this._network!.Status)) {
this._status = "starting";
}
if (networkOfflineStatuses.includes(this._network!.Status)) {
this._status = "offline";
this._icon = mdiCloseCircle;
}
}
private _generateServiceButton(service: string) {
const serviceData = { instance_id: this.ozwInstance };
return html`
<ha-call-service-button
.hass=${this.hass}
domain="ozw"
.service=${service}
.serviceData=${serviceData}
>
${this.hass!.localize(`ui.panel.config.ozw.services.${service}`)}
</ha-call-service-button>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.secondary {
color: var(--secondary-text-color);
}
.online {
color: green;
}
.starting {
color: orange;
}
.offline {
color: red;
}
.content {
margin-top: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
.network-status {
text-align: center;
}
.network-status div.details {
font-size: 1.5rem;
margin-bottom: 16px;
}
.network-status ha-svg-icon {
display: block;
margin: 0px auto 16px;
width: 48px;
height: 48px;
}
.network-status small {
font-size: 1rem;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.card-actions.warning ha-call-service-button {
color: var(--error-color);
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
ha-service-description {
display: block;
color: grey;
padding: 0 8px 12px;
}
[hidden] {
display: none;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-network-dashboard": OZWNetworkDashboard;
}
}

View File

@@ -1,131 +0,0 @@
import "@material/mwc-button/mwc-button";
import { mdiAlert, mdiCheck } from "@mdi/js";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/buttons/ha-call-service-button";
import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/ha-card";
import { fetchOZWNodes, OZWDevice } from "../../../../../data/ozw";
import "../../../../../layouts/hass-tabs-subpage";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { ozwNetworkTabs } from "./ozw-network-router";
export interface NodeRowData extends OZWDevice {
node?: NodeRowData;
id?: number;
}
@customElement("ozw-network-nodes")
class OZWNetworkNodes extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@property() public ozwInstance = 0;
@state() private _nodes: OZWDevice[] = [];
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer => ({
node_id: {
title: this.hass.localize("ui.panel.config.ozw.nodes_table.id"),
sortable: true,
type: "numeric",
width: "72px",
filterable: true,
direction: "asc",
},
node_product_name: {
title: this.hass.localize("ui.panel.config.ozw.nodes_table.model"),
sortable: true,
width: narrow ? "75%" : "25%",
},
node_manufacturer_name: {
title: this.hass.localize(
"ui.panel.config.ozw.nodes_table.manufacturer"
),
sortable: true,
hidden: narrow,
width: "25%",
},
node_query_stage: {
title: this.hass.localize(
"ui.panel.config.ozw.nodes_table.query_stage"
),
sortable: true,
width: narrow ? "25%" : "15%",
},
is_zwave_plus: {
title: this.hass.localize("ui.panel.config.ozw.nodes_table.zwave_plus"),
hidden: narrow,
template: (value: boolean) =>
value ? html` <ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` : "",
},
is_failed: {
title: this.hass.localize("ui.panel.config.ozw.nodes_table.failed"),
hidden: narrow,
template: (value: boolean) =>
value ? html` <ha-svg-icon .path=${mdiAlert}></ha-svg-icon>` : "",
},
})
);
protected firstUpdated() {
if (!this.ozwInstance) {
navigate("/config/ozw/dashboard", { replace: true });
} else if (this.hass) {
this._fetchData();
}
}
protected render(): TemplateResult {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwNetworkTabs(this.ozwInstance)}
.columns=${this._columns(this.narrow)}
.data=${this._nodes}
id="node_id"
@row-click=${this._handleRowClicked}
clickable
>
</hass-tabs-subpage-data-table>
`;
}
private async _fetchData() {
this._nodes = await fetchOZWNodes(this.hass!, this.ozwInstance!);
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const nodeId = ev.detail.id;
navigate(`/config/ozw/network/${this.ozwInstance}/node/${nodeId}`);
}
static get styles(): CSSResultGroup {
return haStyle;
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-network-nodes": OZWNetworkNodes;
}
}

View File

@@ -1,74 +0,0 @@
import { mdiNetwork, mdiServerNetwork } from "@mdi/js";
import { customElement, property } from "lit/decorators";
import {
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../../../../types";
import { computeTail } from "./ozw-config-router";
export const ozwNetworkTabs = (instance: number): PageNavigation[] => [
{
translationKey: "ui.panel.config.ozw.navigation.network",
path: `/config/ozw/network/${instance}/dashboard`,
iconPath: mdiServerNetwork,
},
{
translationKey: "ui.panel.config.ozw.navigation.nodes",
path: `/config/ozw/network/${instance}/nodes`,
iconPath: mdiNetwork,
},
];
@customElement("ozw-network-router")
class OZWNetworkRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public ozwInstance!: number;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
routes: {
dashboard: {
tag: "ozw-network-dashboard",
load: () => import("./ozw-network-dashboard"),
},
nodes: {
tag: "ozw-network-nodes",
load: () => import("./ozw-network-nodes"),
},
node: {
tag: "ozw-node-router",
load: () => import("./ozw-node-router"),
},
},
};
protected updatePageEl(el): void {
el.route = computeTail(this.routeTail);
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
el.ozwInstance = this.ozwInstance;
if (this._currentPage === "node") {
el.nodeId = this.routeTail.path.split("/")[1];
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-network-router": OZWNetworkRouter;
}
}

View File

@@ -1,265 +0,0 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import {
fetchOZWNodeConfig,
fetchOZWNodeMetadata,
fetchOZWNodeStatus,
OZWDevice,
OZWDeviceConfig,
OZWDeviceMetaDataResponse,
} from "../../../../../data/ozw";
import { ERR_NOT_FOUND } from "../../../../../data/websocket_api";
import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { ozwNodeTabs } from "./ozw-node-router";
import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node";
@customElement("ozw-node-config")
class OZWNodeConfig extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@property() public ozwInstance?;
@property() public nodeId?;
@state() private _node?: OZWDevice;
@state() private _metadata?: OZWDeviceMetaDataResponse;
@state() private _config?: OZWDeviceConfig[];
@state() private _error?: string;
protected firstUpdated() {
if (!this.ozwInstance) {
navigate("/config/ozw/dashboard", { replace: true });
} else if (!this.nodeId) {
navigate(`/config/ozw/network/${this.ozwInstance}/nodes`, {
replace: true,
});
} else {
this._fetchData();
}
}
protected render(): TemplateResult {
if (this._error) {
return html`
<hass-error-screen
.hass=${this.hass}
.error=${this.hass.localize(
"ui.panel.config.ozw.node." + this._error
)}
></hass-error-screen>
`;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwNodeTabs(this.ozwInstance, this.nodeId)}
>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.ozw.node_config.header")}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.ozw.node_config.introduction"
)}
<p>
<em>
${this.hass.localize(
"ui.panel.config.ozw.node_config.help_source"
)}
</em>
</p>
<p>
Note: This panel is currently read-only. The ability to change
values will come in a later update.
</p>
</div>
${this._node
? html`
<ha-card class="content">
<div class="card-content">
<b>
${this._node.node_manufacturer_name}
${this._node.node_product_name} </b
><br />
${this.hass.localize("ui.panel.config.ozw.common.node_id")}:
${this._node.node_id}<br />
${this.hass.localize(
"ui.panel.config.ozw.common.query_stage"
)}:
${this._node.node_query_stage}
${this._metadata?.metadata.ProductManualURL
? html` <a
href=${this._metadata.metadata.ProductManualURL}
>
<p>
${this.hass.localize(
"ui.panel.config.ozw.node_metadata.product_manual"
)}
</p>
</a>`
: ``}
</div>
<div class="card-actions">
<mwc-button @click=${this._refreshNodeClicked}>
${this.hass.localize(
"ui.panel.config.ozw.refresh_node.button"
)}
</mwc-button>
</div>
</ha-card>
${this._metadata?.metadata.WakeupHelp
? html`
<ha-card
class="content"
header=${this.hass.localize(
"ui.panel.config.ozw.common.wakeup_instructions"
)}
>
<div class="card-content">
<span class="secondary">
${this.hass.localize(
"ui.panel.config.ozw.node_config.wakeup_help"
)}
</span>
<p>${this._metadata.metadata.WakeupHelp}</p>
</div>
</ha-card>
`
: ``}
${this._config
? html`
${this._config.map(
(item) => html`
<ha-card class="content">
<div class="card-content">
<b>${item.label}</b><br />
<span class="secondary">${item.help}</span>
<p>${item.value}</p>
</div>
</ha-card>
`
)}
`
: ``}
`
: ``}
</ha-config-section>
</hass-tabs-subpage>
`;
}
private async _fetchData() {
if (!this.ozwInstance || !this.nodeId) {
return;
}
try {
const nodeProm = fetchOZWNodeStatus(
this.hass!,
this.ozwInstance,
this.nodeId
);
const metadataProm = fetchOZWNodeMetadata(
this.hass!,
this.ozwInstance,
this.nodeId
);
const configProm = fetchOZWNodeConfig(
this.hass!,
this.ozwInstance,
this.nodeId
);
[this._node, this._metadata, this._config] = await Promise.all([
nodeProm,
metadataProm,
configProm,
]);
} catch (err: any) {
if (err.code === ERR_NOT_FOUND) {
this._error = ERR_NOT_FOUND;
return;
}
throw err;
}
}
private async _refreshNodeClicked() {
showOZWRefreshNodeDialog(this, {
node_id: this.nodeId,
ozw_instance: this.ozwInstance,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.secondary {
color: var(--secondary-text-color);
font-size: 0.9em;
}
.content {
margin-top: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
[hidden] {
display: none;
}
blockquote {
display: block;
background-color: #ddd;
padding: 8px;
margin: 8px 0;
font-size: 0.9em;
}
blockquote em {
font-size: 0.9em;
margin-top: 6px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-node-config": OZWNodeConfig;
}
}

View File

@@ -1,254 +0,0 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import {
fetchOZWNodeMetadata,
fetchOZWNodeStatus,
OZWDevice,
OZWDeviceMetaDataResponse,
} from "../../../../../data/ozw";
import { ERR_NOT_FOUND } from "../../../../../data/websocket_api";
import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { ozwNodeTabs } from "./ozw-node-router";
import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node";
@customElement("ozw-node-dashboard")
class OZWNodeDashboard extends LitElement {
@property({ type: Object }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public configEntryId?: string;
@property() public ozwInstance?;
@property() public nodeId?;
@state() private _node?: OZWDevice;
@state() private _metadata?: OZWDeviceMetaDataResponse;
@state() private _not_found = false;
protected firstUpdated() {
if (!this.ozwInstance) {
navigate("/config/ozw/dashboard", { replace: true });
} else if (!this.nodeId) {
navigate(`/config/ozw/network/${this.ozwInstance}/nodes`, {
replace: true,
});
} else if (this.hass) {
this._fetchData();
}
}
protected render(): TemplateResult {
if (this._not_found) {
return html`
<hass-error-screen
.hass=${this.hass}
.error=${this.hass.localize("ui.panel.config.ozw.node.not_found")}
></hass-error-screen>
`;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${ozwNodeTabs(this.ozwInstance, this.nodeId)}
>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">Node Management</div>
<div slot="introduction">
View the status of a node and manage its configuration.
</div>
${this._node
? html`
<ha-card class="content">
<div class="card-content flex">
<div class="node-details">
<b>
${this._node.node_manufacturer_name}
${this._node.node_product_name}
</b>
<br />
Node ID: ${this._node.node_id}<br />
Query Stage: ${this._node.node_query_stage}
${this._metadata?.metadata.ProductManualURL
? html` <a
href=${this._metadata.metadata.ProductManualURL}
>
<p>Product Manual</p>
</a>`
: ``}
</div>
${this._metadata?.metadata.ProductPicBase64
? html`<img
class="product-image"
src="data:image/png;base64,${this._metadata?.metadata
.ProductPicBase64}"
/>`
: ``}
</div>
<div class="card-actions">
<mwc-button @click=${this._refreshNodeClicked}>
Refresh Node
</mwc-button>
</div>
</ha-card>
${this._metadata
? html`
<ha-card class="content" header="Description">
<div class="card-content">
${this._metadata.metadata.Description}
</div>
</ha-card>
<ha-card class="content" header="Inclusion">
<div class="card-content">
${this._metadata.metadata.InclusionHelp}
</div>
</ha-card>
<ha-card class="content" header="Exclusion">
<div class="card-content">
${this._metadata.metadata.ExclusionHelp}
</div>
</ha-card>
<ha-card class="content" header="Reset">
<div class="card-content">
${this._metadata.metadata.ResetHelp}
</div>
</ha-card>
${this._metadata.metadata.WakeupHelp
? html`
<ha-card class="content" header="WakeUp">
<div class="card-content">
${this._metadata.metadata.WakeupHelp}
</div>
</ha-card>
`
: ``}
`
: ``}
`
: ``}
</ha-config-section>
</hass-tabs-subpage>
`;
}
private async _fetchData() {
if (!this.ozwInstance || !this.nodeId) {
return;
}
try {
this._node = await fetchOZWNodeStatus(
this.hass!,
this.ozwInstance,
this.nodeId
);
this._metadata = await fetchOZWNodeMetadata(
this.hass!,
this.ozwInstance,
this.nodeId
);
} catch (err: any) {
if (err.code === ERR_NOT_FOUND) {
this._not_found = true;
return;
}
throw err;
}
}
private async _refreshNodeClicked() {
showOZWRefreshNodeDialog(this, {
node_id: this.nodeId,
ozw_instance: this.ozwInstance,
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.secondary {
color: var(--secondary-text-color);
}
.content {
margin-top: 24px;
}
.content:last-child {
margin-bottom: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.flex {
display: flex;
justify-content: space-between;
}
.card-actions.warning ha-call-service-button {
color: var(--error-color);
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
ha-service-description {
display: block;
color: grey;
padding: 0 8px 12px;
}
[hidden] {
display: none;
}
.product-image {
padding: 12px;
max-height: 140px;
max-width: 140px;
}
.card-actions {
clear: right;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-node-dashboard": OZWNodeDashboard;
}
}

View File

@@ -1,84 +0,0 @@
import { mdiNetwork, mdiWrench } from "@mdi/js";
import { customElement, property } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import {
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../../../../types";
export const ozwNodeTabs = (
instance: number,
node: number
): PageNavigation[] => [
{
translationKey: "ui.panel.config.ozw.navigation.node.dashboard",
path: `/config/ozw/network/${instance}/node/${node}/dashboard`,
iconPath: mdiNetwork,
},
{
translationKey: "ui.panel.config.ozw.navigation.node.config",
path: `/config/ozw/network/${instance}/node/${node}/config`,
iconPath: mdiWrench,
},
];
@customElement("ozw-node-router")
class OZWNodeRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public ozwInstance!: number;
@property() public nodeId!: number;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
routes: {
dashboard: {
tag: "ozw-node-dashboard",
load: () => import("./ozw-node-dashboard"),
},
config: {
tag: "ozw-node-config",
load: () => import("./ozw-node-config"),
},
},
};
protected updatePageEl(el): void {
el.route = this.routeTail;
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
el.ozwInstance = this.ozwInstance;
el.nodeId = this.nodeId;
const searchParams = new URLSearchParams(window.location.search);
if (this._configEntry && !searchParams.has("config_entry")) {
searchParams.append("config_entry", this._configEntry);
navigate(
`${this.routeTail.prefix}${
this.routeTail.path
}?${searchParams.toString()}`,
{ replace: true }
);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ozw-node-router": OZWNodeRouter;
}
}

View File

@@ -1,19 +0,0 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface OZWRefreshNodeDialogParams {
ozw_instance: number;
node_id: number;
}
export const loadRefreshNodeDialog = () => import("./dialog-ozw-refresh-node");
export const showOZWRefreshNodeDialog = (
element: HTMLElement,
refreshNodeDialogParams: OZWRefreshNodeDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-ozw-refresh-node",
dialogImport: loadRefreshNodeDialog,
dialogParams: refreshNodeDialogParams,
});
};

View File

@@ -1,765 +0,0 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateDomain } from "../../../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../../../common/entity/compute_state_name";
import { sortStatesByName } from "../../../../../common/entity/states_sort_by_name";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-icon";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-icon-button-arrow-prev";
import "../../../../../components/ha-menu-button";
import "../../../../../components/ha-service-description";
import "../../../../../layouts/ha-app-layout";
import { EventsMixin } from "../../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../../mixins/localize-mixin";
import "../../../../../styles/polymer-ha-style";
import "../../../ha-config-section";
import "../../../ha-form-style";
import "./zwave-groups";
import "./zwave-log";
import "./zwave-network";
import "./zwave-node-config";
import "./zwave-node-protection";
import "./zwave-usercodes";
import "./zwave-values";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex ha-style ha-form-style">
app-toolbar {
border-bottom: 1px solid var(--divider-color);
}
ha-alert {
display: block;
margin: 16px;
}
ha-alert a {
text-decoration: none;
}
.content {
margin-top: 24px;
}
.sectionHeader {
position: relative;
padding-right: 40px;
}
.node-info {
margin-left: 16px;
}
.help-text {
padding-left: 24px;
padding-right: 24px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.device-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 24px;
}
ha-service-description {
display: block;
color: grey;
}
ha-service-description[hidden] {
display: none;
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
</style>
<ha-app-layout>
<app-header slot="header" fixed="">
<app-toolbar>
<ha-icon-button-arrow-prev
hass="[[hass]]"
on-click="_backTapped"
></ha-icon-button-arrow-prev>
<div main-title="">[[localize('component.zwave.title')]]</div>
</app-toolbar>
</app-header>
<ha-alert
alert-type="warning"
title="This integration will stop working soon"
>
This Z-Wave integration is deprecated and will no longer receive any
updates. The technical dependencies will render this integration
unusable in the near future. We strongly advise you to migrate to the
new
<a
href="https://www.home-assistant.io/integrations/zwave_js"
target="_blank"
rel="noreferrer"
>Z-Wave JS integration</a
>.
<a
slot="action"
href="https://alerts.home-assistant.io/#zwave.markdown"
target="_blank"
rel="noreferrer"
>
<mwc-button>learn more</mwc-button>
</a>
</ha-alert>
<ha-config-section is-wide="[[isWide]]">
<ha-card
class="content"
header="[[localize('ui.panel.config.zwave.migration.zwave_js.header')]]"
>
<div class="card-content">
[[localize('ui.panel.config.zwave.migration.zwave_js.introduction')]]
</div>
<div class="card-actions">
<a href="/config/zwave/migration"
><mwc-button>Start Migration to Z-Wave JS</mwc-button></a
>
</div>
</ha-card>
</ha-config-section>
<zwave-network
id="zwave-network"
is-wide="[[isWide]]"
hass="[[hass]]"
></zwave-network>
<!-- Node card -->
<ha-config-section is-wide="[[isWide]]">
<div class="sectionHeader" slot="header">
<span
>[[localize('ui.panel.config.zwave.node_management.header')]]</span
>
<ha-icon-button
class="toggle-help-icon"
on-click="toggleHelp"
label="[[localize('ui.common.help')]]"
>
<ha-icon icon="hass:help-circle"></ha-icon>
</ha-icon-button>
</div>
<span slot="introduction">
[[localize('ui.panel.config.zwave.node_management.introduction')]]
</span>
<ha-card class="content">
<div class="device-picker">
<paper-dropdown-menu
dynamic-align=""
label="[[localize('ui.panel.config.zwave.node_management.nodes')]]"
class="flex"
>
<paper-listbox
slot="dropdown-content"
selected="{{selectedNode}}"
>
<template is="dom-repeat" items="[[nodes]]" as="state">
<paper-item>[[computeSelectCaption(state)]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]">
<template is="dom-if" if="[[showHelp]]">
<div style="color: grey; padding: 12px">
[[localize('ui.panel.config.zwave.node_management.introduction')]]
</div>
</template>
</template>
<template is="dom-if" if="[[computeIsNodeSelected(selectedNode)]]">
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="refresh_node"
service-data="[[computeNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.refresh_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="refresh_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<template is="dom-if" if="[[nodeFailed]]">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="remove_failed_node"
service-data="[[computeNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.remove_failed_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="remove_failed_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="replace_failed_node"
service-data="[[computeNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.replace_failed_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="replace_failed_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
</template>
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="print_node"
service-data="[[computeNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.print_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="print_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="heal_node"
service-data="[[computeHealNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.heal_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="heal_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="test_node"
service-data="[[computeNodeServiceData(selectedNode)]]"
>
[[localize('ui.panel.config.zwave.services.test_node')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="test_node"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<mwc-button on-click="_nodeMoreInfo"
>[[localize('ui.panel.config.zwave.services.node_info')]]</mwc-button
>
</div>
<div class="device-picker">
<paper-dropdown-menu
label="[[localize('ui.panel.config.zwave.node_management.entities')]]"
dynamic-align=""
class="flex"
>
<paper-listbox
slot="dropdown-content"
selected="{{selectedEntity}}"
>
<template is="dom-repeat" items="[[entities]]" as="state">
<paper-item>[[state.entity_id]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<template
is="dom-if"
if="[[!computeIsEntitySelected(selectedEntity)]]"
>
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="refresh_entity"
service-data="[[computeRefreshEntityServiceData(selectedEntity)]]"
>
[[localize('ui.panel.config.zwave.services.refresh_entity')]]
</ha-call-service-button>
<ha-service-description
hass="[[hass]]"
domain="zwave"
service="refresh_entity"
hidden$="[[!showHelp]]"
>
</ha-service-description>
<mwc-button on-click="_entityMoreInfo"
>[[localize('ui.panel.config.zwave.node_management.entity_info')]]</mwc-button
>
</div>
<div class="form-group">
<ha-formfield
label="[[localize('ui.panel.config.zwave.node_management.exclude_entity')]]"
>
<ha-checkbox
checked="[[entityIgnored]]"
class="form-control"
on-change="entityIgnoredChanged"
>
</ha-checkbox>
</ha-formfield>
<paper-input
disabled="{{entityIgnored}}"
label="[[localize('ui.panel.config.zwave.node_management.pooling_intensity')]]"
type="number"
min="0"
value="{{entityPollingIntensity}}"
>
</paper-input>
</div>
<div class="card-actions">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="set_poll_intensity"
service-data="[[computePollIntensityServiceData(entityPollingIntensity)]]"
>
[[localize('ui.common.save')]]
</ha-call-service-button>
</div>
</template>
</template>
</ha-card>
<template is="dom-if" if="[[computeIsNodeSelected(selectedNode)]]">
<!-- Value card -->
<zwave-values
hass="[[hass]]"
nodes="[[nodes]]"
selected-node="[[selectedNode]]"
values="[[values]]"
></zwave-values>
<!-- Group card -->
<zwave-groups
hass="[[hass]]"
nodes="[[nodes]]"
selected-node="[[selectedNode]]"
groups="[[groups]]"
></zwave-groups>
<!-- Config card -->
<zwave-node-config
hass="[[hass]]"
nodes="[[nodes]]"
selected-node="[[selectedNode]]"
config="[[config]]"
></zwave-node-config>
</template>
<!-- Protection card -->
<template is="dom-if" if="{{_protectionNode}}">
<zwave-node-protection
hass="[[hass]]"
nodes="[[nodes]]"
selected-node="[[selectedNode]]"
protection="[[_protection]]"
></zwave-node-protection>
</template>
<!-- User Codes -->
<template is="dom-if" if="{{hasNodeUserCodes}}">
<zwave-usercodes
id="zwave-usercodes"
hass="[[hass]]"
nodes="[[nodes]]"
user-codes="[[userCodes]]"
selected-node="[[selectedNode]]"
></zwave-usercodes>
</template>
</ha-config-section>
<!-- Ozw log -->
<ozw-log is-wide="[[isWide]]" hass="[[hass]]"></ozw-log>
</ha-app-layout>
`;
}
static get properties() {
return {
hass: Object,
isWide: Boolean,
nodes: {
type: Array,
computed: "computeNodes(hass)",
},
selectedNode: {
type: Number,
value: -1,
observer: "selectedNodeChanged",
},
nodeFailed: {
type: Boolean,
value: false,
},
config: {
type: Array,
value: () => [],
},
entities: {
type: Array,
computed: "computeEntities(selectedNode)",
},
selectedEntity: {
type: Number,
value: -1,
observer: "selectedEntityChanged",
},
values: {
type: Array,
},
groups: {
type: Array,
},
userCodes: {
type: Array,
value: () => [],
},
hasNodeUserCodes: {
type: Boolean,
value: false,
},
showHelp: {
type: Boolean,
value: false,
},
entityIgnored: Boolean,
entityPollingIntensity: {
type: Number,
value: 0,
},
_protection: {
type: Array,
value: () => [],
},
_protectionNode: {
type: Boolean,
value: false,
},
};
}
ready() {
super.ready();
import("web-animations-js/web-animations-next-lite.min");
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
);
}
attached() {
setCancelSyntheticClickEvents(true);
}
detached() {
setCancelSyntheticClickEvents(false);
}
serviceCalled(ev) {
if (ev.detail.success && ev.detail.service === "set_poll_intensity") {
this._saveEntity();
}
}
computeNodes(hass) {
return Object.keys(hass.states)
.map((key) => hass.states[key])
.filter((ent) => ent.entity_id.match("zwave[.]"))
.sort(sortStatesByName);
}
computeEntities(selectedNode) {
if (!this.nodes || selectedNode === -1) {
return -1;
}
const nodeid = this.nodes[this.selectedNode].attributes.node_id;
const hass = this.hass;
return Object.keys(this.hass.states)
.map((key) => hass.states[key])
.filter((ent) => {
if (ent.attributes.node_id === undefined) {
return false;
}
return (
"node_id" in ent.attributes &&
ent.attributes.node_id === nodeid &&
!ent.entity_id.match("zwave[.]")
);
})
.sort(sortStatesByName);
}
selectedNodeChanged(selectedNode) {
if (selectedNode === -1) {
return;
}
this.selectedEntity = -1;
this.hass
.callApi(
"GET",
`zwave/config/${this.nodes[selectedNode].attributes.node_id}`
)
.then((configs) => {
this.config = this._objToArray(configs);
});
this.hass
.callApi(
"GET",
`zwave/values/${this.nodes[selectedNode].attributes.node_id}`
)
.then((values) => {
this.values = this._objToArray(values);
});
this.hass
.callApi(
"GET",
`zwave/groups/${this.nodes[selectedNode].attributes.node_id}`
)
.then((groups) => {
this.groups = this._objToArray(groups);
});
this.hasNodeUserCodes = false;
this.notifyPath("hasNodeUserCodes");
this.hass
.callApi(
"GET",
`zwave/usercodes/${this.nodes[selectedNode].attributes.node_id}`
)
.then((usercodes) => {
this.userCodes = this._objToArray(usercodes);
this.hasNodeUserCodes = this.userCodes.length > 0;
this.notifyPath("hasNodeUserCodes");
});
this.hass
.callApi(
"GET",
`zwave/protection/${this.nodes[selectedNode].attributes.node_id}`
)
.then((protections) => {
this._protection = this._objToArray(protections);
if (this._protection) {
if (this._protection.length === 0) {
return;
}
this._protectionNode = true;
}
});
this.nodeFailed = this.nodes[selectedNode].attributes.is_failed;
}
selectedEntityChanged(selectedEntity) {
if (selectedEntity === -1) {
return;
}
this.hass
.callApi(
"GET",
`zwave/values/${this.nodes[this.selectedNode].attributes.node_id}`
)
.then((values) => {
this.values = this._objToArray(values);
});
const valueId = this.entities[selectedEntity].attributes.value_id;
const valueData = this.values.find((obj) => obj.key === valueId);
const valueIndex = this.values.indexOf(valueData);
this.hass
.callApi(
"GET",
`config/zwave/device_config/${this.entities[selectedEntity].entity_id}`
)
.then((data) => {
this.setProperties({
entityIgnored: data.ignored || false,
entityPollingIntensity: this.values[valueIndex].value.poll_intensity,
});
})
.catch(() => {
this.setProperties({
entityIgnored: false,
entityPollingIntensity: this.values[valueIndex].value.poll_intensity,
});
});
}
computeSelectCaption(stateObj) {
return (
computeStateName(stateObj) +
" (Node:" +
stateObj.attributes.node_id +
" " +
stateObj.attributes.query_stage +
")"
);
}
computeSelectCaptionEnt(stateObj) {
return computeStateDomain(stateObj) + "." + computeStateName(stateObj);
}
computeIsNodeSelected() {
return this.nodes && this.selectedNode !== -1;
}
computeIsEntitySelected(selectedEntity) {
return selectedEntity === -1;
}
computeNodeServiceData(selectedNode) {
return { node_id: this.nodes[selectedNode].attributes.node_id };
}
computeHealNodeServiceData(selectedNode) {
return {
node_id: this.nodes[selectedNode].attributes.node_id,
return_routes: true,
};
}
computeRefreshEntityServiceData(selectedEntity) {
if (selectedEntity === -1) {
return -1;
}
return { entity_id: this.entities[selectedEntity].entity_id };
}
computePollIntensityServiceData(entityPollingIntensity) {
if (this.selectedNode === -1 || this.selectedEntity === -1) {
return -1;
}
return {
node_id: this.nodes[this.selectedNode].attributes.node_id,
value_id: this.entities[this.selectedEntity].attributes.value_id,
poll_intensity: parseInt(entityPollingIntensity),
};
}
_nodeMoreInfo() {
this.fire("hass-more-info", {
entityId: this.nodes[this.selectedNode].entity_id,
});
}
_entityMoreInfo() {
this.fire("hass-more-info", {
entityId: this.entities[this.selectedEntity].entity_id,
});
}
_saveEntity() {
const data = {
ignored: this.entityIgnored,
polling_intensity: parseInt(this.entityPollingIntensity),
};
return this.hass.callApi(
"POST",
`config/zwave/device_config/${
this.entities[this.selectedEntity].entity_id
}`,
data
);
}
toggleHelp() {
this.showHelp = !this.showHelp;
}
_objToArray(obj) {
const array = [];
Object.keys(obj).forEach((key) => {
array.push({
key,
value: obj[key],
});
});
return array;
}
_backTapped() {
history.back();
}
entityIgnoredChanged(ev) {
this.entityIgnored = ev.target.checked;
}
}
customElements.define("ha-config-zwave", HaConfigZwave);

View File

@@ -1,62 +0,0 @@
import { customElement, property } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import {
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../../../types";
@customElement("zwave-config-router")
class ZWaveConfigRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
routes: {
dashboard: {
tag: "ha-config-zwave",
load: () =>
import(/* webpackChunkName: "ha-config-zwave" */ "./ha-config-zwave"),
},
migration: {
tag: "zwave-migration",
load: () =>
import(/* webpackChunkName: "zwave-migration" */ "./zwave-migration"),
},
},
};
protected updatePageEl(el): void {
el.route = this.routeTail;
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
el.configEntryId = this._configEntry;
const searchParams = new URLSearchParams(window.location.search);
if (this._configEntry && !searchParams.has("config_entry")) {
searchParams.append("config_entry", this._configEntry);
navigate(
`${this.routeTail.prefix}${
this.routeTail.path
}?${searchParams.toString()}`,
{ replace: true }
);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"zwave-config-router": ZWaveConfigRouter;
}
}

View File

@@ -1,380 +0,0 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateName } from "../../../../../common/entity/compute_state_name";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import LocalizeMixin from "../../../../../mixins/localize-mixin";
import "../../../../../styles/polymer-ha-style";
class ZwaveGroups extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
margin-top: 24px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.device-picker {
@apply --layout-horizontal;
@apply --layout-center-center;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 24px;
}
.help-text {
padding-left: 24px;
padding-right: 24px;
padding-bottom: 12px;
}
</style>
<ha-card
class="content"
header="[[localize('ui.panel.config.zwave.node_management.node_group_associations')]]"
>
<!-- TODO make api for getting groups and members -->
<div class="device-picker">
<paper-dropdown-menu
label="[[localize('ui.panel.config.zwave.node_management.group')]]"
dynamic-align=""
class="flex"
>
<paper-listbox
slot="dropdown-content"
selected="{{_selectedGroup}}"
>
<template is="dom-repeat" items="[[groups]]" as="state">
<paper-item>[[_computeSelectCaptionGroup(state)]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<template is="dom-if" if="[[_computeIsGroupSelected(_selectedGroup)]]">
<div class="device-picker">
<paper-dropdown-menu
label="[[localize('ui.panel.config.zwave.node_management.node_to_control')]]"
dynamic-align=""
class="flex"
>
<paper-listbox
slot="dropdown-content"
selected="{{_selectedTargetNode}}"
>
<template is="dom-repeat" items="[[nodes]]" as="state">
<paper-item>[[_computeSelectCaption(state)]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<div class="help-text">
<span
>[[localize('ui.panel.config.zwave.node_management.nodes_in_group')]]</span
>
<template is="dom-repeat" items="[[_otherGroupNodes]]" as="state">
<div>[[state]]</div>
</template>
</div>
<div class="help-text">
<span
>[[localize('ui.panel.config.zwave.node_management.max_associations')]]</span
>
<span>[[_maxAssociations]]</span>
</div>
</template>
<template
is="dom-if"
if="[[_computeIsTargetNodeSelected(_selectedTargetNode)]]"
>
<div class="card-actions">
<template is="dom-if" if="[[!_noAssociationsLeft]]">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="change_association"
service-data="[[_addAssocServiceData]]"
>
[[localize('ui.panel.config.zwave.node_management.add_to_group')]]
</ha-call-service-button>
</template>
<template
is="dom-if"
if="[[_computeTargetInGroup(_selectedGroup, _selectedTargetNode)]]"
>
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="change_association"
service-data="[[_removeAssocServiceData]]"
>
[[localize('ui.panel.config.zwave.node_management.remove_from_group')]]
</ha-call-service-button>
</template>
<template is="dom-if" if="[[_isBroadcastNodeInGroup]]">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="change_association"
service-data="[[_removeBroadcastNodeServiceData]]"
>
[[localize('ui.panel.config.zwave.node_management.remove_broadcast')]]
</ha-call-service-button>
</template>
</div>
</template>
</ha-card>
`;
}
static get properties() {
return {
hass: Object,
nodes: Array,
groups: Array,
selectedNode: {
type: Number,
observer: "_selectedNodeChanged",
},
_selectedTargetNode: {
type: Number,
value: -1,
observer: "_selectedTargetNodeChanged",
},
_selectedGroup: {
type: Number,
value: -1,
},
_otherGroupNodes: {
type: Array,
value: -1,
computed: "_computeOtherGroupNodes(_selectedGroup)",
},
_maxAssociations: {
type: String,
value: "",
computed: "_computeMaxAssociations(_selectedGroup)",
},
_noAssociationsLeft: {
type: Boolean,
value: true,
computed: "_computeAssociationsLeft(_selectedGroup)",
},
_addAssocServiceData: {
type: String,
value: "",
},
_removeAssocServiceData: {
type: String,
value: "",
},
_removeBroadcastNodeServiceData: {
type: String,
value: "",
},
_isBroadcastNodeInGroup: {
type: Boolean,
value: false,
},
};
}
static get observers() {
return ["_selectedGroupChanged(groups, _selectedGroup)"];
}
ready() {
super.ready();
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
);
}
serviceCalled(ev) {
if (ev.detail.success) {
setTimeout(() => {
this._refreshGroups(this.selectedNode);
}, 5000);
}
}
_computeAssociationsLeft(selectedGroup) {
if (selectedGroup === -1) return true;
return this._maxAssociations === this._otherGroupNodes.length;
}
_computeMaxAssociations(selectedGroup) {
if (selectedGroup === -1) return -1;
const maxAssociations = this.groups[selectedGroup].value.max_associations;
if (!maxAssociations) return "None";
return maxAssociations;
}
_computeOtherGroupNodes(selectedGroup) {
if (selectedGroup === -1) return -1;
this.setProperties({ _isBroadcastNodeInGroup: false });
const associations = Object.values(
this.groups[selectedGroup].value.association_instances
);
if (!associations.length) return ["None"];
return associations.map((assoc) => {
if (!assoc.length || assoc.length !== 2) {
return `Unknown Node: ${assoc}`;
}
const id = assoc[0];
const instance = assoc[1];
const node = this.nodes.find((n) => n.attributes.node_id === id);
if (id === 255) {
this.setProperties({
_isBroadcastNodeInGroup: true,
_removeBroadcastNodeServiceData: {
node_id: this.nodes[this.selectedNode].attributes.node_id,
association: "remove",
target_node_id: 255,
group: this.groups[selectedGroup].key,
},
});
}
if (!node) {
return `Unknown Node (${id}: (${instance} ? ${id}.${instance} : ${id}))`;
}
let caption = this._computeSelectCaption(node);
if (instance) {
caption += `/ Instance: ${instance}`;
}
return caption;
});
}
_computeTargetInGroup(selectedGroup, selectedTargetNode) {
if (selectedGroup === -1 || selectedTargetNode === -1) return false;
const associations = Object.values(
this.groups[selectedGroup].value.associations
);
if (!associations.length) return false;
return (
associations.indexOf(
this.nodes[selectedTargetNode].attributes.node_id
) !== -1
);
}
_computeSelectCaption(stateObj) {
return `${computeStateName(stateObj)}
(Node: ${stateObj.attributes.node_id}
${stateObj.attributes.query_stage})`;
}
_computeSelectCaptionGroup(stateObj) {
return `${stateObj.key}: ${stateObj.value.label}`;
}
_computeIsTargetNodeSelected(selectedTargetNode) {
return this.nodes && selectedTargetNode !== -1;
}
_computeIsGroupSelected(selectedGroup) {
return this.nodes && this.selectedNode !== -1 && selectedGroup !== -1;
}
_computeAssocServiceData(selectedGroup, type) {
if (
!this.groups ||
selectedGroup === -1 ||
this.selectedNode === -1 ||
this._selectedTargetNode === -1
) {
return -1;
}
return {
node_id: this.nodes[this.selectedNode].attributes.node_id,
association: type,
target_node_id: this.nodes[this._selectedTargetNode].attributes.node_id,
group: this.groups[selectedGroup].key,
};
}
async _refreshGroups(selectedNode) {
const groupData = [];
const groups = await this.hass.callApi(
"GET",
`zwave/groups/${this.nodes[selectedNode].attributes.node_id}`
);
Object.keys(groups).forEach((key) => {
groupData.push({
key,
value: groups[key],
});
});
this.setProperties({
groups: groupData,
_maxAssociations: groupData[this._selectedGroup].value.max_associations,
_otherGroupNodes: Object.values(
groupData[this._selectedGroup].value.associations
),
_isBroadcastNodeInGroup: false,
});
const oldGroup = this._selectedGroup;
this.setProperties({ _selectedGroup: -1 });
this.setProperties({ _selectedGroup: oldGroup });
}
_selectedGroupChanged() {
if (this._selectedGroup === -1) return;
this.setProperties({
_maxAssociations: this.groups[this._selectedGroup].value.max_associations,
_otherGroupNodes: Object.values(
this.groups[this._selectedGroup].value.associations
),
});
}
_selectedTargetNodeChanged() {
if (this._selectedGroup === -1) return;
if (
this._computeTargetInGroup(this._selectedGroup, this._selectedTargetNode)
) {
this.setProperties({
_removeAssocServiceData: this._computeAssocServiceData(
this._selectedGroup,
"remove"
),
});
} else {
this.setProperties({
_addAssocServiceData: this._computeAssocServiceData(
this._selectedGroup,
"add"
),
});
}
}
_selectedNodeChanged() {
if (this.selectedNode === -1) return;
this.setProperties({ _selectedTargetNode: -1, _selectedGroup: -1 });
}
}
customElements.define("zwave-groups", ZwaveGroups);

View File

@@ -1,83 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../../../../../mixins/events-mixin";
import "../../../../../styles/polymer-ha-style-dialog";
import "../../../../../components/ha-dialog";
class ZwaveLogDialog extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style-dialog">
pre {
font-family: var(--code-font-family, monospace);
}
</style>
<ha-dialog open="[[_opened]]" heading="OpenZwave internal logfile" on-closed="closeDialog">
<div>
<pre>[[_ozwLog]]</pre>
<div>
</ha-dialog>
`;
}
static get properties() {
return {
hass: Object,
_ozwLog: String,
_dialogClosedCallback: Function,
_opened: {
type: Boolean,
value: false,
},
_intervalId: String,
_numLogLines: {
type: Number,
},
};
}
ready() {
super.ready();
this.addEventListener("iron-overlay-closed", (ev) =>
this._dialogClosed(ev)
);
}
showDialog({ _ozwLog, hass, _tail, _numLogLines, dialogClosedCallback }) {
this.hass = hass;
this._ozwLog = _ozwLog;
this._opened = true;
this._dialogClosedCallback = dialogClosedCallback;
this._numLogLines = _numLogLines;
if (_tail) {
this.setProperties({
_intervalId: setInterval(() => {
this._refreshLog();
}, 1500),
});
}
}
closeDialog() {
clearInterval(this._intervalId);
this._opened = false;
const closedEvent = true;
this._dialogClosedCallback({ closedEvent });
this._dialogClosedCallback = null;
}
async _refreshLog() {
const info = await this.hass.callApi(
"GET",
"zwave/ozwlog?lines=" + this._numLogLines
);
this.setProperties({ _ozwLog: info });
}
}
customElements.define("zwave-log-dialog", ZwaveLogDialog);

View File

@@ -1,160 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import isPwa from "../../../../../common/config/is_pwa";
import "../../../../../components/ha-card";
import { EventsMixin } from "../../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../../mixins/localize-mixin";
import "../../../../../styles/polymer-ha-style";
import "../../../ha-config-section";
let registeredDialog = false;
class OzwLog extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
margin-top: 24px;
}
ha-card {
margin: 0 auto;
max-width: 600px;
}
.device-picker {
padding-left: 24px;
padding-right: 24px;
padding-bottom: 24px;
}
</style>
<ha-config-section is-wide="[[isWide]]">
<span slot="header">
[[localize('ui.panel.config.zwave.ozw_log.header')]]
</span>
<span slot="introduction">
[[localize('ui.panel.config.zwave.ozw_log.introduction')]]
</span>
<ha-card class="content">
<div class="device-picker">
<paper-input label="[[localize('ui.panel.config.zwave.ozw_log.last_log_lines')]]" type="number" min="0" max="1000" step="10" value="{{numLogLines}}">
</paper-input>
</div>
<div class="card-actions">
<mwc-button raised="true" on-click="_openLogWindow">[[localize('ui.panel.config.zwave.ozw_log.load')]]</mwc-button>
<mwc-button raised="true" on-click="_tailLog" disabled="{{_completeLog}}">[[localize('ui.panel.config.zwave.ozw_log.tail')]]</mwc-button>
</ha-card>
</ha-config-section>
`;
}
static get properties() {
return {
hass: Object,
isWide: {
type: Boolean,
value: false,
},
_ozwLogs: String,
_completeLog: {
type: Boolean,
value: true,
},
numLogLines: {
type: Number,
value: 0,
observer: "_isCompleteLog",
},
_intervalId: String,
tail: Boolean,
};
}
async _tailLog() {
this.setProperties({ tail: true });
const ozwWindow = await this._openLogWindow();
if (!isPwa()) {
this.setProperties({
_intervalId: setInterval(() => {
this._refreshLog(ozwWindow);
}, 1500),
});
}
}
async _openLogWindow() {
const info = await this.hass.callApi(
"GET",
"zwave/ozwlog?lines=" + this.numLogLines
);
this.setProperties({ _ozwLogs: info });
if (isPwa()) {
this._showOzwlogDialog();
return -1;
}
const ozwWindow = open("", "ozwLog", "toolbar");
ozwWindow.document.body.innerHTML = `<pre>${this._ozwLogs}</pre>`;
return ozwWindow;
}
async _refreshLog(ozwWindow) {
if (ozwWindow.closed === true) {
clearInterval(this._intervalId);
this.setProperties({ _intervalId: null });
} else {
const info = await this.hass.callApi(
"GET",
"zwave/ozwlog?lines=" + this.numLogLines
);
this.setProperties({ _ozwLogs: info });
ozwWindow.document.body.innerHTML = `<pre>${this._ozwLogs}</pre>`;
}
}
_isCompleteLog() {
if (this.numLogLines !== "0") {
this.setProperties({ _completeLog: false });
} else {
this.setProperties({ _completeLog: true });
}
}
connectedCallback() {
super.connectedCallback();
if (!registeredDialog) {
registeredDialog = true;
this.fire("register-dialog", {
dialogShowEvent: "show-ozwlog-dialog",
dialogTag: "zwave-log-dialog",
dialogImport: () => import("./zwave-log-dialog"),
});
}
}
_showOzwlogDialog() {
this.fire("show-ozwlog-dialog", {
hass: this.hass,
_numLogLines: this.numLogLines,
_ozwLog: this._ozwLogs,
_tail: this.tail,
dialogClosedCallback: () => this._dialogClosed(),
});
}
_dialogClosed() {
this.setProperties({
tail: false,
});
}
}
customElements.define("ozw-log", OzwLog);

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