Compare commits

...

162 Commits

Author SHA1 Message Date
Bram Kragten
7b057eaa77 Merge pull request #5024 from home-assistant/dev
20200228.0
2020-02-28 22:59:33 +01:00
Paulus Schoutsen
d7aaed05b7 Clean up generic row (#5022) 2020-02-28 13:35:42 -08:00
Bram Kragten
c5fe5565bb Merge branch 'master' into dev 2020-02-28 22:21:45 +01:00
Bram Kragten
a1a1763897 Bumped version to 20200228.0 2020-02-28 22:20:44 +01:00
Bram Kragten
724357683c Add take control for yaml mode (#4992) 2020-02-28 22:00:01 +01:00
Bram Kragten
0d6de9fe73 Fix for unavailable input-select (#4991)
* Fix for unavailable select

* Update hui-thermostat-card.ts
2020-02-28 21:59:14 +01:00
Bram Kragten
5646045e9e Add UI to create and manage Lovelace dashboards and resources (#5012)
* Add UI to create and manage Lovelace dashboards and resources

* update, comments, fixes

* Align icons with seach icon and checkboxes

* Fix

* Remove js and html resource types

* Allow it for existing ones
2020-02-28 21:58:50 +01:00
Bram Kragten
17c7a3bbac Bumped version to 20200220.5 2020-02-28 15:35:08 +01:00
Ian Richardson
8d65eb1fdf Fix state_color for button (#4995) 2020-02-28 15:35:04 +01:00
Ian Richardson
2298a55b16 Fix action handling for buttons row (#5007) 2020-02-28 15:34:45 +01:00
HomeAssistant Azure
33d65bcefc [ci skip] Translation update 2020-02-28 00:32:34 +00:00
Thomas Lovén
3cc7deda04 GUI editors for stacks (#4999)
* GUI editors for stacks

* fix type checking

* lint

* Address review comments

* Cleanup. Removing inline functions, combining others

* Give the class a more fitting name

* Final tweak

* Update stack cards

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-02-27 14:07:55 +01:00
Bram Kragten
e2de660bec Only cache default lovelace and handle config updates (#5000)
* Only cache default lovelace and handle config updates

* Update partial-panel-resolver.ts
2020-02-27 14:01:37 +01:00
Bram Kragten
6b1e5a525f Fix update if no lastversion and change audio pickers to default (#5010) 2020-02-27 11:44:09 +01:00
Ian Richardson
93565f0ed9 Fix action handling for buttons row (#5007) 2020-02-27 09:20:20 +01:00
HomeAssistant Azure
143d1162b6 [ci skip] Translation update 2020-02-27 00:32:30 +00:00
Ian Richardson
788d616fa2 Fix state_color for button (#4995) 2020-02-26 23:03:47 +01:00
Bram Kragten
0de9471a5d Revert "Static import all the LL cards etc" (#4989)
This reverts commit 52ded635ff.
2020-02-26 08:03:05 -08:00
Bram Kragten
b229071248 Add helper UI (#4940)
* Add helper UI

* Oops

* Update

* Update

* Update

* Lint

* Add all input forms

* Return extended entity registry entry from update

* Comments

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-02-26 12:53:03 +01:00
Bram Kragten
6d145730a5 Bumped version to 20200220.4 2020-02-26 09:42:10 +01:00
Paulus Schoutsen
f02bb67485 Static import all the LL cards etc (#4987) 2020-02-26 09:42:03 +01:00
Paulus Schoutsen
52ded635ff Static import all the LL cards etc (#4987) 2020-02-26 09:40:54 +01:00
HomeAssistant Azure
a6d73828b8 [ci skip] Translation update 2020-02-26 00:32:31 +00:00
Paulus Schoutsen
1d052fa5bb Add support for multiple Lovelace dashboards (#4967)
* Add support for multiple Lovelace dashboards

* Fix navigation, add to cast, revert resource loading

* Change resource logic

* Lint + cast fix

* Comments

* Fixes

* Console.bye

* Lint"

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-02-25 13:06:25 -08:00
Bram Kragten
38d758b52f Bumped version to 20200220.3 2020-02-25 21:56:52 +01:00
Bram Kragten
9162e9c318 Bumped version to 20200220.2 2020-02-25 21:55:36 +01:00
Bram Kragten
189ea00768 Fix for when the preview element was an error element (#4969)
* Fix for when the preview element was an error element

* Comments

* Update hui-dialog-edit-card.ts
2020-02-25 21:51:52 +01:00
Bram Kragten
25d6427aed Fix for when the preview element was an error element (#4969)
* Fix for when the preview element was an error element

* Comments

* Update hui-dialog-edit-card.ts
2020-02-25 21:51:07 +01:00
Franck Nijhof
8a61442cf2 Add dev demo builds GitHub Actions workflow (#4980) 2020-02-25 10:15:48 -08:00
Franck Nijhof
106d405699 Only trigger on PRs or pushes against the master and dev branch… (#4982) 2020-02-25 10:06:14 -08:00
Chris Talkington
1f23e9062f Fix typo in supervisor popup (#4966) 2020-02-24 20:45:08 -08:00
Franck Nijhof
231b498ea5 Remove Travis-CI (#4972) 2020-02-24 20:43:47 -08:00
HomeAssistant Azure
a256e5abfa [ci skip] Translation update 2020-02-25 00:33:05 +00:00
Ruslan Sayfutdinov
028b370ead [logbook] fix scrolling on iOS (#4950)
* [logbook] fix scrolling on iOS

* Update styling

* Update ha-logbook.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-02-24 13:50:22 +01:00
Sergey Avdeev
18abc6adf7 Update icon_color_css.ts (#4962) 2020-02-24 11:43:58 +01:00
Ruslan Sayfutdinov
95aa29d6ca Fix layout of history and logbook filters (#4963) 2020-02-24 11:38:05 +01:00
Paulus Schoutsen
5d2242dd16 Add a smarter default value for entity card show header toggle (#4964) 2020-02-24 11:29:40 +01:00
HomeAssistant Azure
de8bca6967 [ci skip] Translation update 2020-02-24 00:32:30 +00:00
HomeAssistant Azure
12234de20e [ci skip] Translation update 2020-02-23 00:32:34 +00:00
Tomasz
b41369a2ad Localize tabs names in view editor (#4954)
* Localize tabs

* remove Polish localization
2020-02-22 15:00:01 +01:00
HomeAssistant Azure
6e35c79c14 [ci skip] Translation update 2020-02-22 00:32:31 +00:00
Franck Nijhof
22e4c0512e Add GitHub Actions (#4952) 2020-02-21 13:59:09 +01:00
HomeAssistant Azure
3606b8077f [ci skip] Translation update 2020-02-21 00:34:40 +00:00
Paulus Schoutsen
3a90a65ba8 Merge pull request #4948 from home-assistant/dev
20200220.1
2020-02-20 14:49:22 -08:00
Paulus Schoutsen
e59987a8ed Bumped version to 20200220.1 2020-02-20 14:47:40 -08:00
Paulus Schoutsen
22d8ce0fd9 Fix creating card (#4947) 2020-02-20 14:47:28 -08:00
Zep Fietje
01eae3876b Make config list items appearance consistent for automations, scenes and scripts (#4945) 2020-02-20 21:10:40 +01:00
Paulus Schoutsen
2e43f390a4 Merge pull request #4942 from home-assistant/dev
20200220.0
2020-02-20 10:29:20 -08:00
Paulus Schoutsen
65421fa551 Bumped version to 20200220.0 2020-02-20 10:14:57 -08:00
Zack Arnett
fc88922ce3 Fix CPU and Browser Usuage (#4935) 2020-02-20 09:57:14 +01:00
Paulus Schoutsen
52609dded9 Add rebuild support to editor preview (#4932)
* Add rebuild support to editor preview

* getLovelaceCardClass function added

* Use error class

* Tiny cleanup

* Misplaced comment
2020-02-20 09:55:42 +01:00
HomeAssistant Azure
6d54496187 [ci skip] Translation update 2020-02-20 00:32:31 +00:00
Bram Kragten
2a6c38066d Merge pull request #4920 from home-assistant/dev
20200219.0
2020-02-19 10:49:19 +01:00
Bram Kragten
924c7804c9 Update hui-media-control-card.ts (#4919) 2020-02-19 10:44:42 +01:00
Bram Kragten
23f34fa7ae Bumped version to 20200219.0 2020-02-19 10:30:14 +01:00
Ian Richardson
7046cba1f7 Add buttons row (#4714)
*  Add buttons row

* refactor to composition

* Add action handler

* address review
2020-02-19 10:25:42 +01:00
Zack Arnett
4be1040a14 Convert History Graph Card to Typescript (#4882)
* Converting History Graph to Typescript

* Conversion to TS

* Reviews

* Review Updates
2020-02-19 10:20:37 +01:00
Zack Arnett
68baeb83cb Media Card Seek Functionality (#4907)
* Seek function

* Remove a testing section

* reviews
2020-02-19 10:19:57 +01:00
Zack Arnett
aa94e45582 Sensor Card - Top Margin 16px -> 8px (#4917) 2020-02-18 22:36:49 -08:00
Zack Arnett
2c58a9f802 Sensor Stroke Width Fix (#4916)
* Sensor width fix

* Const
2020-02-18 20:20:32 -08:00
HomeAssistant Azure
0a41a4f066 [ci skip] Translation update 2020-02-19 00:32:35 +00:00
Bram Kragten
e265d9581c Fix multiselect without data (#4906) 2020-02-18 22:17:52 +01:00
Paulus Schoutsen
4675579f79 Add safe mode support (#4908)
* Add safe mode support

* Lint

* Fix type in demo
2020-02-18 08:33:25 -08:00
Bram Kragten
52ae01ea74 Fix description and title of options flow (#4910) 2020-02-18 11:27:27 +01:00
HomeAssistant Azure
099430238c [ci skip] Translation update 2020-02-18 00:32:41 +00:00
Bram Kragten
af3626b215 Merge pull request #4850 from zsarnett/media-card-updates
Update Media Card to check for Supported Features
2020-02-17 21:16:06 +01:00
Bram Kragten
52ea3a5ce8 Merge pull request #4895 from scop/sort-available-events
Sort list of available events
2020-02-17 21:15:08 +01:00
Ville Skyttä
2ab2ade642 Import compare 2020-02-17 21:15:58 +02:00
Zack Arnett
1cc3936ec3 Typing functions 2020-02-17 13:36:21 -05:00
Paulus Schoutsen
322eef1c0f Add more info to system log (#4899)
* Add more info to system log

* Add util, oops

* Tweak UI
2020-02-17 10:20:24 -08:00
Zack Arnett
0964130782 rookie mistake 2020-02-17 12:09:17 -05:00
Zack Arnett
be9ec50e3a When no image occurs mark the image as undefined 2020-02-17 11:51:05 -05:00
Zack Arnett
da1dd45169 Update for playlist information if music isnt present 2020-02-17 11:31:27 -05:00
Zack Arnett
9a7f7f119d Review updates again because I am a fool 2020-02-17 10:28:13 -05:00
Bram Kragten
b1a414c840 Merge pull request #4903 from home-assistant/dev
20200217.0
2020-02-17 16:24:27 +01:00
Zack Arnett
2718ada9f9 Review updates 2020-02-17 10:01:01 -05:00
Bram Kragten
46a596ce34 Bumped version to 20200217.0 2020-02-17 15:58:06 +01:00
Bram Kragten
7036cefa72 Allow to change the location of home zone in zone editor (#4849)
* Allow to change the location of home zone in zone editor

* Update src/translations/en.json

Co-Authored-By: Paulus Schoutsen <balloob@gmail.com>

* Comment + mobile to general config

* Remove dupe import

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-02-17 15:27:31 +01:00
Ian Richardson
fb7fbf2dac add state_color option to glance, button and state-icon (#4854)
* add state_color option to glance, button and state-icon

* address comments

* address comments
2020-02-17 15:26:45 +01:00
Bram Kragten
49b0c8d549 include entities not in entity registry in config entities (#4867)
* include entities not in entity registry in config entities

* Update ha-data-table.ts

* Comments

* Update ha-device-entities-card.ts

* Comments
2020-02-17 15:02:23 +01:00
Bram Kragten
8f9a6bd544 Add multi select component to ha-form (#4247)
* Add multi select component

* Apply suggestions from code review

Co-Authored-By: Ian Richardson <iantrich@gmail.com>

* Comments

* update

* Fix

* Refactor to dropdown menu

Co-authored-by: Ian Richardson <iantrich@gmail.com>
2020-02-17 14:13:09 +01:00
Ruslan Sayfutdinov
24e4b0b772 [sidebar] set max-width for item-text (#4893) 2020-02-16 20:23:25 -08:00
HomeAssistant Azure
1c86bd2f8b [ci skip] Translation update 2020-02-17 00:32:55 +00:00
Zack Arnett
363f548f13 fix error (#4885) 2020-02-16 15:19:28 -08:00
Ville Skyttä
51ce481e77 Sort list of available events 2020-02-16 23:32:47 +02:00
Zack Arnett
30e5611812 Stupid mistake 2020-02-16 12:15:56 -05:00
Zack Arnett
67706a312d Support for requesting the image from the server 2020-02-16 12:11:18 -05:00
Paulus Schoutsen
f4eb3380b4 Less whitespace around icon (#4888) 2020-02-15 22:39:03 -08:00
Paulus Schoutsen
73934afc7d Sensor card hugging (#4887) 2020-02-16 00:57:56 -05:00
Paulus Schoutsen
9d2a0c0502 Reduce size of floorplan pic Arsaboo (#4886) 2020-02-15 21:40:28 -08:00
HomeAssistant Azure
9ec75531a8 [ci skip] Translation update 2020-02-16 00:32:37 +00:00
HomeAssistant Azure
91bdb8f742 [ci skip] Translation update 2020-02-15 00:32:38 +00:00
Thomas Lovén
d8ae3439de Make sure config is always frozen. Not just on error (#4871) 2020-02-14 16:11:25 -08:00
Bram Kragten
2d018fff6c Update babel en disable ES5 builds on dev (#4876) 2020-02-14 16:09:21 -08:00
Bram Kragten
7d37dc6cde Bump vaadin elements (#4878) 2020-02-14 16:08:44 -08:00
Bram Kragten
c60033027d Update material elements (#4877)
* Update material elements

* Update ha-checkbox.ts
2020-02-14 16:08:30 -08:00
Ian Richardson
3f7c29a6f6 ♻️ change entity-button to button card (#4581)
* ♻️ change entity-button to button card

* maintain separate entity-button class
2020-02-14 10:56:08 +01:00
Paulus Schoutsen
b2243f480c Add option to lazy load cards (#4857)
* Add option to lazy load cards

* Lazy load header/footer elements

* Lazy load rows

* Clean up params

* Rename last var
2020-02-13 21:13:48 -08:00
Ian Richardson
f5384e8bc8 add create helper functions for custom cards (#4853)
* add create helper functions for custom cards

* address comments
2020-02-13 21:13:29 -08:00
Robbie Trencheny
ecc6fcf862 Hide HTML5 push notification toggle if inside external app (#4860)
* Hide HTML5 push notification toggle if external bus is engaged

* Use isExternal instead

* Hide the whole row

* Black

* Fix import

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-02-13 17:47:08 -08:00
HomeAssistant Azure
46cc2aec94 [ci skip] Translation update 2020-02-14 00:32:50 +00:00
Thomas Lovén
c62a5a6dcd Freeze lovelace configuration on load (#4862)
* Freeze lovelace configuration on load

* Clone only when necessary

* Make cloning badges really work

* Freeze after checking

* Don't doublefreeze
2020-02-13 16:18:15 -08:00
Zack Arnett
f6b10232ec Disabled Style Updates 2020-02-13 16:35:16 -05:00
Zack Arnett
87559c0938 Review Updates 2020-02-13 16:30:34 -05:00
Bram Kragten
7903541689 Add toolbars and mobile headers + layout tweaks (#4803)
* Add toolbars and mobile headers + layout tweaks

* Comments
2020-02-13 19:53:48 +01:00
Bram Kragten
c93e1b0123 Add more info for Person (#4848)
* Add more info for Person

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

Co-Authored-By: Paulus Schoutsen <balloob@gmail.com>

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-02-13 09:06:52 -08:00
Bram Kragten
e261fafdb3 Allow enabling/disabling automation from edit screen (#4846)
* Allow enabling/disabling automation from edit screen

* Comments
2020-02-13 09:06:14 -08:00
Bram Kragten
485e2fde25 Update hui-thermostat-card.ts (#4863) 2020-02-13 08:41:51 -08:00
Bram Kragten
6feaf64c90 Fix display of arrays/objects in attributes (#4836)
* Fix display of arrays/objects in attributes

* async import js-yaml
2020-02-13 08:22:41 -08:00
Bram Kragten
6b115bf06a Merge pull request #4859 from home-assistant/frenck-2020-0188
Spelling: Config(uration)
2020-02-13 10:29:46 +01:00
Bram Kragten
f45785fafe Change defaults of automations to device (#4843)
* Change defaults of automations to device

* Script too

* Update device_automation.ts

* Update device_automation.ts
2020-02-12 20:43:44 -08:00
Ruslan Sayfutdinov
ec046bc925 [history] fix dropdown animation (#4858) 2020-02-12 20:36:29 -08:00
HomeAssistant Azure
ab5733718b [ci skip] Translation update 2020-02-13 00:33:01 +00:00
Franck Nijhof
1077fb2945 Spelling: Config(uration) 2020-02-13 00:30:41 +01:00
Bram Kragten
b7a84cdd60 Merge pull request #4856 from home-assistant/dev
20200212.0
2020-02-12 22:41:36 +01:00
Bram Kragten
78102f5882 Merge branch 'master' into dev 2020-02-12 22:21:32 +01:00
Bram Kragten
4ea11bd928 Bumped version to 20200212.0 2020-02-12 22:18:01 +01:00
Bram Kragten
785aefa028 Bumped version to 20200130.3 2020-02-12 21:58:59 +01:00
Bram Kragten
5c2004bcc1 Bumped version to 20200130.2 2020-02-12 21:56:03 +01:00
Bram Kragten
156d944ca1 Fix translations for update button config entry system options (#4837) 2020-02-12 21:55:53 +01:00
Bram Kragten
97a6354a72 Fix tap firing twice on iOS (#4841) 2020-02-12 21:55:28 +01:00
Bram Kragten
49422c3f63 Fix translations for update button config entry system options (#4837) 2020-02-12 13:49:20 +01:00
Bram Kragten
0b8700f725 Hide zone edit button for non admins (#4840) 2020-02-12 13:48:51 +01:00
Bram Kragten
c5aa000a97 Check for null target_temp (#4842)
Fixes #4359
2020-02-12 13:48:27 +01:00
Bram Kragten
4cdc4765f7 Fix tap firing twice on iOS (#4841) 2020-02-12 13:47:56 +01:00
Ruslan Sayfutdinov
a95290235d [logbook] fix margins for RTL languages (#4852) 2020-02-11 17:29:35 -08:00
HomeAssistant Azure
fb9d7ac2d8 [ci skip] Translation update 2020-02-12 00:32:46 +00:00
Ruslan Sayfutdinov
d48a4e0ac6 [logbook] implement shouldUpdate (#4832)
* [logbook] implement shouldUpdate

* Update src/panels/logbook/ha-logbook.ts

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-02-11 15:26:28 -08:00
Zack Arnett
d33e035db7 Update Media Card to check for supported features 2020-02-11 13:11:25 -05:00
Bram Kragten
1437b4c4b6 Fix media control card styling (#4845) 2020-02-11 09:43:46 -05:00
Robert Resch
9fce60065b Add lozalization to more-info-vacuum (#4793)
* Convert more-info-vacuum to Lit

* Introduce VacuumCommand to reduce render complexity and duplicate code

* Add localization for more-info-vacuum

* Inline supportFeature instead of creating a const.

* - Use interface instead of class for VacuumCommand
- Add different translation for start_pause and pause as they are not the same

* fix typo

* Use VACUUM_COMMANDS.some instead of writing duplicate code

* add @bramkragten suggestions
2020-02-11 09:15:38 +01:00
Paulus Schoutsen
d052b9ede8 Update en.json (#4835) 2020-02-11 09:05:18 +01:00
Ruslan Sayfutdinov
8cee5c729e [logbook] fix period dropdown animation (#4834) 2020-02-10 20:50:12 -08:00
HomeAssistant Azure
88bdf7c7ec [ci skip] Translation update 2020-02-11 00:32:36 +00:00
Bram Kragten
2c006e99f2 Use original id to remove entity (#4829) 2020-02-10 15:17:29 -08:00
Erik Montnemery
e7e8dff0ec Graceful fallback if translations for device automations are missing (#4824)
* Graceful fallback if translations for device automations are missing

* Update src/data/device_automation.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update src/data/device_automation.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* Update src/data/device_automation.ts

Co-Authored-By: Bram Kragten <mail@bramkragten.nl>

* tweak

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-02-10 20:17:24 +01:00
Ian Richardson
981c798e22 💄 match alarm panel icon coloring to card's (#4825) 2020-02-10 08:43:38 -08:00
Ruslan Sayfutdinov
4613d8b1f6 [logbook] configure flex container to display entries correctly on mobile devices (#4810) 2020-02-10 10:11:28 +01:00
Paulus Schoutsen
ba4e1949c4 Theme update refetch themes (#4812) 2020-02-10 10:10:39 +01:00
Iulian Onofrei
cc6686a790 Fix typo (#4779) 2020-02-10 10:03:42 +01:00
Nicholas Amadori
f791412f73 Spelling (#4806)
"Gerenal" corrected to "General"
2020-02-10 10:03:19 +01:00
HomeAssistant Azure
0c8ac17dcb [ci skip] Translation update 2020-02-10 00:32:29 +00:00
HomeAssistant Azure
9e11fe868e [ci skip] Translation update 2020-02-09 00:32:35 +00:00
Bram Kragten
7d91515bf5 Style tweaks (#4766)
* Style tweaks

* Update ha-style.ts

* Move derived styles
2020-02-08 14:49:29 -08:00
HomeAssistant Azure
e0565c35ab [ci skip] Translation update 2020-02-08 00:32:33 +00:00
Paulus Schoutsen
e5387e5806 Fall back to use handler if translations broken (#4777) 2020-02-07 18:46:16 +01:00
Paulus Schoutsen
8a4c52aeb7 Filter battery sensors from generated UI (#4799)
* Filter battery sensors from generated UI

* Use fancy TypeScript feature
2020-02-07 18:32:37 +01:00
Paulus Schoutsen
15e7b8117c Add integrations to dev info page (#4800)
* Add integrations to dev info page

* Fix annoying looking html tags

* What happened here
2020-02-07 18:30:38 +01:00
HomeAssistant Azure
d1703ba3e8 [ci skip] Translation update 2020-02-07 00:32:35 +00:00
Ruslan Sayfutdinov
c977f22047 Show seconds in the UI (#4765) 2020-02-06 10:38:38 -08:00
Zack Arnett
2e47aa1905 Update Edit Footer for Cards (#4752)
* Update CSS

* Radius Updates

* Updating to be a ha-card element
2020-02-05 23:05:49 -08:00
HomeAssistant Azure
c72105dca3 [ci skip] Translation update 2020-02-06 00:32:31 +00:00
Bram Kragten
e01f1cfcac More info scroll fix (#4774)
* Fix more info dialog scrolling

* Update ha-more-info-dialog.js
2020-02-05 14:03:16 -08:00
Bram Kragten
50d0671abe Bumped version to 20200130.1 2020-02-04 15:38:03 +01:00
Bram Kragten
e176357fbf Tweak badge focus padding (#4750) 2020-02-04 15:34:20 +01:00
David F. Mulcahey
de1b127ac2 fix loading groups (#4727) 2020-02-04 15:33:50 +01:00
Bram Kragten
1dad7c81da Fix passive color radius and fix switch label clicks (#4703) 2020-02-04 15:33:01 +01:00
Bram Kragten
e980e93969 Change map settings icon (#4701)
* Change map settings icon

Closes #4694

* hide for demo
2020-02-04 15:28:59 +01:00
Ian Richardson
57fc56f836 🐛 fix tabindex for default entity more-info actions (#4697)
* 🐛 fix tabindex for default entity more-info actions

* Update hui-state-label-badge.ts
2020-02-04 15:28:33 +01:00
Bram Kragten
05113e1809 Styling zone menu (#4684)
* Styling zone menu

* Update ha-device-entities-card.ts
2020-02-04 15:28:10 +01:00
Bram Kragten
1479ce9d56 Merge pull request #4672 from home-assistant/dev
20200130.0
2020-01-30 17:37:25 +01:00
Bram Kragten
ce8caa34f5 Merge pull request #4649 from home-assistant/dev
20200129.0
2020-01-29 19:48:36 +01:00
305 changed files with 14452 additions and 4184 deletions

127
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,127 @@
name: CI
on:
push:
branches:
- dev
- master
pull_request:
branches:
- dev
- master
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v2
- name: Setting up Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Get yarn cache path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Build icons
run: ./node_modules/.bin/gulp gen-icons-hassio gen-icons-mdi gen-icons-app
- name: Build translations
run: ./node_modules/.bin/gulp build-translations
- name: Run eslint
run: ./node_modules/.bin/eslint src hassio/src gallery/src
- name: Run tslint
run: ./node_modules/.bin/tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts'
- name: Run tsc
run: ./node_modules/.bin/tsc
test:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v2
- name: Setting up Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Get yarn cache path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Run Mocha
run: npm run mocha
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- name: Check out files from GitHub
uses: actions/checkout@v2
- name: Setting up Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Get yarn cache path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Build Application
run: ./node_modules/.bin/gulp build-app
env:
TRAVIS: "true"
supervisor:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- name: Check out files from GitHub
uses: actions/checkout@v2
- name: Setting up Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Get yarn cache path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Build Application
run: ./node_modules/.bin/gulp build-hassio
env:
TRAVIS: "true"

39
.github/workflows/demo.yaml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Demo
on:
push:
branches:
- dev
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v2
- name: Setting up Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Get yarn cache path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install
env:
CI: true
- name: Build Demo
run: ./node_modules/.bin/gulp build-demo
- name: Deploy to Netlify
uses: netlify/actions/cli@master
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
with:
args: deploy --dir=demo/dist --prod

View File

@@ -1,18 +0,0 @@
sudo: false
language: node_js
cache:
yarn: true
directories:
- bower_components
install: yarn install
script:
- npm run build
- hassio/script/build_hassio
# Because else eslint fails because hassio has cleaned that build
- ./node_modules/.bin/gulp gen-icons-app
- npm run test
# - xvfb-run wct --module-resolution=node --npm
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
dist: trusty
addons:
sauce_connect: true

View File

@@ -34,6 +34,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
},
],
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
[
require("@babel/plugin-proposal-decorators").default,
{ decoratorsBeforeExport: true },

View File

@@ -57,7 +57,7 @@ const handler = (done) => (err, stats) => {
gulp.task("webpack-watch-app", () => {
// we are not calling done, so this command will run forever
webpack(bothBuilds(createAppConfig, { isProdBuild: false })).watch(
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
{},
handler()
);

View File

@@ -15,6 +15,7 @@ import {
import {
LovelaceConfig,
getLovelaceCollection,
fetchResources,
} from "../../../../src/data/lovelace";
import "./hc-launch-screen";
import { castContext } from "../cast_context";
@@ -23,6 +24,8 @@ import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages";
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
let resourcesLoaded = false;
@customElement("hc-main")
export class HcMain extends HassElement {
@property() private _showDemo = false;
@@ -34,6 +37,7 @@ export class HcMain extends HassElement {
@property() private _error?: string;
private _unsubLovelace?: UnsubscribeFunc;
private _urlPath?: string | null;
public processIncomingMessage(msg: HassMessage) {
if (msg.type === "connect") {
@@ -108,6 +112,7 @@ export class HcMain extends HassElement {
if (this.hass) {
status.hassUrl = this.hass.auth.data.hassUrl;
status.lovelacePath = this._lovelacePath!;
status.urlPath = this._urlPath;
}
if (senderId) {
@@ -163,8 +168,19 @@ export class HcMain extends HassElement {
this._error = "Cannot show Lovelace because we're not connected.";
return;
}
if (!this._unsubLovelace) {
const llColl = getLovelaceCollection(this.hass!.connection);
if (!resourcesLoaded) {
resourcesLoaded = true;
loadLovelaceResources(
await fetchResources(this.hass!.connection),
this.hass!.auth.data.hassUrl
);
}
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
this._urlPath = msg.urlPath;
if (this._unsubLovelace) {
this._unsubLovelace();
}
const llColl = getLovelaceCollection(this.hass!.connection, msg.urlPath);
// We first do a single refresh because we need to check if there is LL
// configuration.
try {
@@ -194,12 +210,6 @@ export class HcMain extends HassElement {
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
castContext.setApplicationState(lovelaceConfig.title!);
this._lovelaceConfig = lovelaceConfig;
if (lovelaceConfig.resources) {
loadLovelaceResources(
lovelaceConfig.resources,
this.hass!.auth.data.hassUrl
);
}
}
private _handleShowDemo(_msg: ShowDemoMessage) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -395,7 +395,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
cards: [
{
entity: "script.air_cleaner_quiet",
type: "entity-button",
type: "button",
name: "AC bed",
tap_action: {
action: "call-service",
@@ -408,7 +408,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
{
entity: "script.air_cleaner_auto",
type: "entity-button",
type: "button",
name: "AC bed",
tap_action: {
action: "call-service",
@@ -421,7 +421,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
{
entity: "script.air_cleaner_turbo",
type: "entity-button",
type: "button",
name: "AC bed",
tap_action: {
action: "call-service",
@@ -434,7 +434,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
{
entity: "script.ac_off",
type: "entity-button",
type: "button",
name: "AC",
tap_action: {
action: "call-service",
@@ -447,7 +447,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
{
entity: "script.ac_on",
type: "entity-button",
type: "button",
name: "AC",
tap_action: {
action: "call-service",
@@ -658,7 +658,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
action: "call-service",
service: "script.goodnight",
},
type: "entity-button",
type: "button",
icon: "mdi:weather-night",
},
{
@@ -670,7 +670,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
service: "scene.turn_on",
},
type: "entity-button",
type: "button",
icon: "mdi:coffee-outline",
},
{
@@ -682,7 +682,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
service: "scene.turn_on",
},
type: "entity-button",
type: "button",
icon: "mdi:television-classic",
},
],
@@ -743,7 +743,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
service: "light.toggle",
},
type: "entity-button",
type: "button",
icon: "mdi:page-layout-footer",
},
{
@@ -755,7 +755,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
service: "light.toggle",
},
type: "entity-button",
type: "button",
icon: "mdi:page-layout-header",
},
],

View File

@@ -15,14 +15,14 @@ const CONFIGS = [
{
heading: "Basic example",
config: `
- type: entity-button
- type: button
entity: light.bed_light
`,
},
{
heading: "With Name",
config: `
- type: entity-button
- type: button
name: Bedroom
entity: light.bed_light
`,
@@ -30,7 +30,7 @@ const CONFIGS = [
{
heading: "With Icon",
config: `
- type: entity-button
- type: button
entity: light.bed_light
icon: mdi:hotel
`,
@@ -38,7 +38,7 @@ const CONFIGS = [
{
heading: "Without State",
config: `
- type: entity-button
- type: button
entity: light.bed_light
show_state: false
`,
@@ -46,7 +46,7 @@ const CONFIGS = [
{
heading: "Custom Tap Action (toggle)",
config: `
- type: entity-button
- type: button
entity: light.bed_light
tap_action:
action: toggle
@@ -55,7 +55,7 @@ const CONFIGS = [
{
heading: "Running Service",
config: `
- type: entity-button
- type: button
entity: light.bed_light
service: light.toggle
`,
@@ -63,13 +63,13 @@ const CONFIGS = [
{
heading: "Invalid Entity",
config: `
- type: entity-button
- type: button
entity: sensor.invalid_entity
`,
},
];
class DemoEntityButtonEntity extends PolymerElement {
class DemoButtonEntity extends PolymerElement {
static get template() {
return html`
<demo-cards
@@ -97,4 +97,4 @@ class DemoEntityButtonEntity extends PolymerElement {
}
}
customElements.define("demo-hui-entity-button-card", DemoEntityButtonEntity);
customElements.define("demo-hui-button-card", DemoButtonEntity);

View File

@@ -128,22 +128,27 @@ class HassioAddonAudio extends LitElement {
private _setInputDevice(ev): void {
const device = ev.detail.item.getAttribute("device");
this._selectedInput = device || null;
this._selectedInput = device;
}
private _setOutputDevice(ev): void {
const device = ev.detail.item.getAttribute("device");
this._selectedOutput = device || null;
this._selectedOutput = device;
}
private async _addonChanged(): Promise<void> {
this._selectedInput = this.addon.audio_input;
this._selectedOutput = this.addon.audio_output;
this._selectedInput =
this.addon.audio_input === null ? "default" : this.addon.audio_input;
this._selectedOutput =
this.addon.audio_output === null ? "default" : this.addon.audio_output;
if (this._outputDevices) {
return;
}
const noDevice: HassioHardwareAudioDevice = { device: null, name: "-" };
const noDevice: HassioHardwareAudioDevice = {
device: "default",
name: "Default",
};
try {
const { audio } = await fetchHassioHardwareAudio(this.hass);
@@ -168,8 +173,10 @@ class HassioAddonAudio extends LitElement {
private async _saveSettings(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
audio_input: this._selectedInput || null,
audio_output: this._selectedOutput || null,
audio_input:
this._selectedInput === "default" ? null : this._selectedInput,
audio_output:
this._selectedOutput === "default" ? null : this._selectedOutput,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);

View File

@@ -452,7 +452,7 @@ class HassioAddonInfo extends LitElement {
`
: ""}
<ha-progress-button
.disabled=${!this.addon.available}
.disabled=${!this.addon.available || this._installing}
.progress=${this._installing}
@click=${this._installClicked}
>

View File

@@ -42,7 +42,9 @@ export class HassioUpdate extends LitElement {
!!value &&
(value.last_version
? value.version !== value.last_version
: value.version !== value.version_latest)
: value.version_latest
? value.version !== value.version_latest
: false)
);
}).length;
@@ -102,7 +104,7 @@ export class HassioUpdate extends LitElement {
releaseNotesUrl: string,
icon?: string
): TemplateResult {
if (lastVersion === curVersion) {
if (!lastVersion || lastVersion === curVersion) {
return html``;
}
return html`

View File

@@ -148,7 +148,7 @@ class HassioSupervisorInfo extends LitElement {
!confirm(`WARNING:
Beta releases are for testers and early adopters and can contain unstable code changes. Make sure you have backups of your data before you activate this feature.
This inludes beta releases for:
This includes beta releases for:
- Home Assistant (Release Candidates)
- Hass.io supervisor
- Host system`)

View File

@@ -18,15 +18,15 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"@material/chips": "^3.2.0",
"@material/data-table": "^3.2.0",
"@material/mwc-base": "^0.10.0",
"@material/mwc-button": "^0.10.0",
"@material/mwc-checkbox": "^0.10.0",
"@material/mwc-dialog": "^0.10.0",
"@material/mwc-fab": "^0.10.0",
"@material/mwc-ripple": "^0.10.0",
"@material/mwc-switch": "^0.10.0",
"@material/chips": "^5.0.0",
"@material/data-table": "^5.0.0",
"@material/mwc-base": "^0.13.0",
"@material/mwc-button": "^0.13.0",
"@material/mwc-checkbox": "^0.13.0",
"@material/mwc-dialog": "^0.13.0",
"@material/mwc-fab": "^0.13.0",
"@material/mwc-ripple": "^0.13.0",
"@material/mwc-switch": "^0.13.0",
"@mdi/svg": "4.9.95",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-localize-behavior": "^3.0.1",
@@ -70,8 +70,8 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.3.7",
"@vaadin/vaadin-combo-box": "^5.0.6",
"@vaadin/vaadin-date-picker": "^4.0.3",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@webcomponents/shadycss": "^1.9.0",
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
@@ -79,12 +79,13 @@
"codemirror": "^5.49.0",
"cpx": "^1.5.0",
"deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1",
"es6-object-assign": "^1.1.0",
"fecha": "^3.0.2",
"fuse.js": "^3.4.4",
"google-timezones-json": "^1.0.2",
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^4.4.0",
"home-assistant-js-websocket": "4.4.1",
"intl-messageformat": "^2.2.0",
"js-yaml": "^3.13.1",
"leaflet": "^1.4.0",
@@ -108,16 +109,17 @@
"xss": "^1.0.6"
},
"devDependencies": {
"@babel/core": "^7.7.4",
"@babel/plugin-external-helpers": "^7.7.4",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-decorators": "^7.7.4",
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
"@babel/plugin-proposal-optional-chaining": "^7.7.4",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-react-jsx": "^7.7.4",
"@babel/preset-env": "^7.7.4",
"@babel/preset-typescript": "^7.7.4",
"@babel/core": "^7.8.4",
"@babel/plugin-external-helpers": "^7.8.3",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-react-jsx": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/preset-typescript": "^7.8.3",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1",
@@ -185,7 +187,15 @@
"resolutions": {
"@webcomponents/webcomponentsjs": "^2.2.10",
"@polymer/polymer": "3.1.0",
"lit-html": "^1.1.2"
"lit-html": "^1.1.2",
"@material/button": "^5.0.0",
"@material/checkbox": "^5.0.0",
"@material/dialog": "^5.0.0",
"@material/fab": "^5.0.0",
"@material/switch": "^5.0.0",
"@material/ripple": "^5.0.0",
"@material/dom": "^5.0.0",
"@material/touch-target": "^5.0.0"
},
"main": "src/home-assistant.js",
"husky": {

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20200130.0",
version="20200228.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@@ -21,6 +21,7 @@ export interface ConnectMessage extends BaseCastMessage {
export interface ShowLovelaceViewMessage extends BaseCastMessage {
type: "show_lovelace_view";
viewPath: string | number | null;
urlPath: string | null;
}
export interface ShowDemoMessage extends BaseCastMessage {
@@ -43,11 +44,13 @@ export const castSendAuth = (cast: CastManager, auth: Auth) =>
export const castSendShowLovelaceView = (
cast: CastManager,
viewPath: ShowLovelaceViewMessage["viewPath"]
viewPath: ShowLovelaceViewMessage["viewPath"],
urlPath?: string | null
) =>
cast.sendMessage({
type: "show_lovelace_view",
viewPath,
urlPath: urlPath || null,
});
export const castSendShowDemo = (cast: CastManager) =>

View File

@@ -8,6 +8,7 @@ export interface ReceiverStatusMessage extends BaseCastMessage {
showDemo: boolean;
hassUrl?: string;
lovelacePath?: string | number | null;
urlPath?: string | null;
}
export type SenderMessage = ReceiverStatusMessage;

View File

@@ -44,6 +44,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"light",
"lock",
"media_player",
"person",
"script",
"sun",
"timer",

View File

@@ -0,0 +1,31 @@
// Check for support of native locale string options
function checkToLocaleDateStringSupportsOptions() {
try {
new Date().toLocaleDateString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
function checkToLocaleTimeStringSupportsOptions() {
try {
new Date().toLocaleTimeString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
function checkToLocaleStringSupportsOptions() {
try {
new Date().toLocaleString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
export const toLocaleDateStringSupportsOptions = checkToLocaleDateStringSupportsOptions();
export const toLocaleTimeStringSupportsOptions = checkToLocaleTimeStringSupportsOptions();
export const toLocaleStringSupportsOptions = checkToLocaleStringSupportsOptions();

View File

@@ -1,20 +1,11 @@
import fecha from "fecha";
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
// Check for support of native locale string options
function toLocaleDateStringSupportsOptions() {
try {
new Date().toLocaleDateString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
export default toLocaleDateStringSupportsOptions()
export const formatDate = toLocaleDateStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleDateString(locales, {
year: "numeric",
month: "long",
day: "numeric",
})
: (dateObj: Date) => fecha.format(dateObj, "mediumDate");
: (dateObj: Date) => fecha.format(dateObj, "longDate");

View File

@@ -1,16 +1,7 @@
import fecha from "fecha";
import { toLocaleStringSupportsOptions } from "./check_options_support";
// Check for support of native locale string options
function toLocaleStringSupportsOptions() {
try {
new Date().toLocaleString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
export default toLocaleStringSupportsOptions()
export const formatDateTime = toLocaleStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleString(locales, {
year: "numeric",
@@ -19,4 +10,24 @@ export default toLocaleStringSupportsOptions()
hour: "numeric",
minute: "2-digit",
})
: (dateObj: Date) => fecha.format(dateObj, "haDateTime");
: (dateObj: Date) =>
fecha.format(
dateObj,
`${fecha.masks.longDate}, ${fecha.masks.shortTime}`
);
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleString(locales, {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
})
: (dateObj: Date) =>
fecha.format(
dateObj,
`${fecha.masks.longDate}, ${fecha.masks.mediumTime}`
);

View File

@@ -1,19 +1,19 @@
import fecha from "fecha";
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
// Check for support of native locale string options
function toLocaleTimeStringSupportsOptions() {
try {
new Date().toLocaleTimeString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
export default toLocaleTimeStringSupportsOptions()
export const formatTime = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
hour: "numeric",
minute: "2-digit",
})
: (dateObj: Date) => fecha.format(dateObj, "shortTime");
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
})
: (dateObj: Date) => fecha.format(dateObj, "mediumTime");

View File

@@ -4,7 +4,7 @@ export const dynamicElement = directive(
(tag: string, properties?: { [key: string]: any }) => (part: Part): void => {
if (!(part instanceof NodePart)) {
throw new Error(
"dynamicContentDirective can only be used in content bindings"
"dynamicElementDirective can only be used in content bindings"
);
}

View File

@@ -1,8 +1,8 @@
import { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
import formatDateTime from "../datetime/format_date_time";
import formatDate from "../datetime/format_date";
import formatTime from "../datetime/format_time";
import { formatDateTime } from "../datetime/format_date_time";
import { formatDate } from "../datetime/format_date";
import { formatTime } from "../datetime/format_time";
import { LocalizeFunc } from "../translations/localize";
export const computeStateDisplay = (

View File

@@ -23,7 +23,7 @@ const fixedIcons = {
homeassistant: "hass:home-assistant",
homekit: "hass:home-automation",
image_processing: "hass:image-filter-frames",
input_boolean: "hass:drawing",
input_boolean: "hass:toggle-switch-outline",
input_datetime: "hass:calendar-clock",
input_number: "hass:ray-vertex",
input_select: "hass:format-list-bulleted",

View File

@@ -7,14 +7,18 @@ import {
property,
} from "lit-element";
import { fireEvent } from "../dom/fire_event";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-icon-button/paper-icon-button";
import "@material/mwc-button";
import "../../components/ha-icon";
import { classMap } from "lit-html/directives/class-map";
@customElement("search-input")
class SearchInput extends LitElement {
@property() public filter?: string;
@property({ type: Boolean, attribute: "no-label-float" })
public noLabelFloat? = false;
@property({ type: Boolean, attribute: "no-underline" })
public noUnderline = false;
public focus() {
this.shadowRoot!.querySelector("paper-input")!.focus();
@@ -22,18 +26,24 @@ class SearchInput extends LitElement {
protected render(): TemplateResult {
return html`
<style>
.no-underline {
--paper-input-container-underline: {
display: none;
height: 0;
}
}
</style>
<div class="search-container">
<paper-input
class=${classMap({ "no-underline": this.noUnderline })}
autofocus
label="Search"
.value=${this.filter}
@value-changed=${this._filterInputChanged}
.noLabelFloat=${this.noLabelFloat}
>
<iron-icon
icon="hass:magnify"
slot="prefix"
class="prefix"
></iron-icon>
<ha-icon icon="hass:magnify" slot="prefix" class="prefix"></ha-icon>
${this.filter &&
html`
<paper-icon-button

View File

@@ -1,7 +1,6 @@
import { css } from "lit-element";
export const iconColorCSS = css`
ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"],
ha-icon[data-domain="alert"][data-state="on"],
ha-icon[data-domain="automation"][data-state="on"],
ha-icon[data-domain="binary_sensor"][data-state="on"],
@@ -30,6 +29,38 @@ export const iconColorCSS = css`
color: var(--heat-color, #ff8100);
}
ha-icon[data-domain="climate"][data-state="drying"] {
color: var(--dry-color, #efbd07);
}
ha-icon[data-domain="alarm_control_panel"] {
color: var(--alarm-color-armed, var(--label-badge-red));
}
ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
color: var(--alarm-color-disarmed, var(--label-badge-green));
}
ha-icon[data-domain="alarm_control_panel"][data-state="pending"],
ha-icon[data-domain="alarm_control_panel"][data-state="arming"] {
color: var(--alarm-color-pending, var(--label-badge-yellow));
animation: pulse 1s infinite;
}
ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
color: var(--alarm-color-triggered, var(--label-badge-red));
animation: pulse 1s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
ha-icon[data-domain="plant"][data-state="problem"],
ha-icon[data-domain="zwave"][data-state="dead"] {
color: var(--error-state-color, #db4437);

View File

@@ -80,6 +80,7 @@ export interface DataTableColumnData extends DataTableSortColumnData {
export interface DataTableRowData {
[key: string]: any;
selectable?: boolean;
}
@customElement("ha-data-table")
@@ -101,6 +102,8 @@ export class HaDataTable extends BaseElement {
@property({ type: String }) private _sortColumn?: string;
@property({ type: String }) private _sortDirection: SortingDirection = null;
@property({ type: Array }) private _filteredData: DataTableRowData[] = [];
@query("slot[name='header']") private _header!: HTMLSlotElement;
@query(".scroller") private _scroller!: HTMLDivElement;
private _sortColumns: {
[key: string]: DataTableSortColumnData;
} = {};
@@ -170,7 +173,7 @@ export class HaDataTable extends BaseElement {
protected render() {
return html`
<div class="mdc-data-table">
<slot name="header">
<slot name="header" @slotchange=${this._calcScrollHeight}>
${this._filterable
? html`
<div class="table-header">
@@ -181,112 +184,116 @@ export class HaDataTable extends BaseElement {
`
: ""}
</slot>
<table class="mdc-data-table__table">
<thead>
<tr class="mdc-data-table__header-row">
${this.selectable
? html`
<div class="scroller">
<table class="mdc-data-table__table">
<thead>
<tr class="mdc-data-table__header-row">
${this.selectable
? html`
<th
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
role="columnheader"
scope="col"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleHeaderRowCheckboxChange}
.indeterminate=${this._headerIndeterminate}
.checked=${this._headerChecked}
>
</ha-checkbox>
</th>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
const sorted = key === this._sortColumn;
const classes = {
"mdc-data-table__header-cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__header-cell--icon": Boolean(
column.type && column.type === "icon"
),
sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted),
};
return html`
<th
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
class="mdc-data-table__header-cell ${classMap(classes)}"
role="columnheader"
scope="col"
@click=${this._handleHeaderClick}
data-column-id="${key}"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleHeaderRowCheckboxChange}
.indeterminate=${this._headerIndeterminate}
.checked=${this._headerChecked}
>
</ha-checkbox>
${column.sortable
? html`
<ha-icon
.icon=${sorted && this._sortDirection === "desc"
? "hass:arrow-down"
: "hass:arrow-up"}
></ha-icon>
`
: ""}
<span>${column.title}</span>
</th>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
const sorted = key === this._sortColumn;
const classes = {
"mdc-data-table__header-cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__header-cell--icon": Boolean(
column.type && column.type === "icon"
),
sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted),
};
return html`
<th
class="mdc-data-table__header-cell ${classMap(classes)}"
role="columnheader"
scope="col"
@click=${this._handleHeaderClick}
data-column-id="${key}"
`;
})}
</tr>
</thead>
<tbody class="mdc-data-table__content">
${repeat(
this._filteredData!,
(row: DataTableRowData) => row[this.id],
(row: DataTableRowData) => html`
<tr
data-row-id="${row[this.id]}"
@click=${this._handleRowClick}
class="mdc-data-table__row"
.selectable=${row.selectable !== false}
>
${column.sortable
${this.selectable
? html`
<ha-icon
.icon=${sorted && this._sortDirection === "desc"
? "hass:arrow-down"
: "hass:arrow-up"}
></ha-icon>
<td
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxChange}
.disabled=${row.selectable === false}
.checked=${this._checkedRows.includes(
String(row[this.id])
)}
>
</ha-checkbox>
</td>
`
: ""}
<span>${column.title}</span>
</th>
`;
})}
</tr>
</thead>
<tbody class="mdc-data-table__content">
${repeat(
this._filteredData!,
(row: DataTableRowData) => row[this.id],
(row: DataTableRowData) => html`
<tr
data-row-id="${row[this.id]}"
@click=${this._handleRowClick}
class="mdc-data-table__row"
>
${this.selectable
? html`
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
return html`
<td
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type && column.type === "icon"
),
})}"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxChange}
.checked=${this._checkedRows.includes(
String(row[this.id])
)}
>
</ha-checkbox>
${column.template
? column.template(row[key], row)
: row[key]}
</td>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
return html`
<td
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type && column.type === "icon"
),
})}"
>
${column.template
? column.template(row[key], row)
: row[key]}
</td>
`;
})}
</tr>
`
)}
</tbody>
</table>
`;
})}
</tr>
`
)}
</tbody>
</table>
</div>
</div>
`;
}
@@ -294,9 +301,12 @@ export class HaDataTable extends BaseElement {
protected createAdapter(): MDCDataTableAdapter {
return {
addClassAtRowIndex: (rowIndex: number, cssClasses: string) => {
if (!(this.rowElements[rowIndex] as any).selectable) {
return;
}
this.rowElements[rowIndex].classList.add(cssClasses);
},
getRowCount: () => this.data.length,
getRowCount: () => this.rowElements.length,
getRowElements: () => this.rowElements,
getRowIdAtIndex: (rowIndex: number) => this._getRowIdAtIndex(rowIndex),
getRowIndexByChildElement: (el: Element) =>
@@ -305,7 +315,7 @@ export class HaDataTable extends BaseElement {
isCheckboxAtRowIndexChecked: (rowIndex: number) =>
this._checkedRows.includes(this._getRowIdAtIndex(rowIndex)),
isHeaderRowCheckboxChecked: () => this._headerChecked,
isRowsSelectable: () => true,
isRowsSelectable: () => this.selectable,
notifyRowSelectionChanged: () => undefined,
notifySelectedAll: () => undefined,
notifyUnselectedAll: () => undefined,
@@ -328,6 +338,9 @@ export class HaDataTable extends BaseElement {
this._headerIndeterminate = indeterminate;
},
setRowCheckboxCheckedAtIndex: (rowIndex: number, checked: boolean) => {
if (!(this.rowElements[rowIndex] as any).selectable) {
return;
}
this._setRowChecked(this._getRowIdAtIndex(rowIndex), checked);
},
};
@@ -434,6 +447,11 @@ export class HaDataTable extends BaseElement {
this._debounceSearch(ev.detail.value);
}
private async _calcScrollHeight() {
await this.updateComplete;
this._scroller.style.maxHeight = `calc(100% - ${this._header.clientHeight}px)`;
}
static get styles(): CSSResult {
return css`
/* default mdc styles, colors changed, without checkbox styles */
@@ -507,6 +525,7 @@ export class HaDataTable extends BaseElement {
padding-left: 16px;
/* @noflip */
padding-right: 0;
width: 40px;
}
[dir="rtl"] .mdc-data-table__header-cell--checkbox,
.mdc-data-table__header-cell--checkbox[dir="rtl"],
@@ -549,6 +568,19 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__cell--icon {
color: var(--secondary-text-color);
text-align: center;
width: 24px;
}
.mdc-data-table__header-cell--icon {
text-align: center;
}
.mdc-data-table__cell--icon:first-child ha-icon {
margin-left: 8px;
}
.mdc-data-table__cell--icon:first-child state-badge {
margin-right: -8px;
}
.mdc-data-table__header-cell {
@@ -578,42 +610,66 @@ export class HaDataTable extends BaseElement {
text-align: left;
}
.mdc-data-table__header-cell--icon {
text-align: center;
}
/* custom from here */
:host {
display: block;
}
.mdc-data-table {
display: block;
border-width: var(--data-table-border-width, 1px);
height: 100%;
}
.mdc-data-table__header-cell {
overflow: hidden;
position: relative;
}
.mdc-data-table__header-cell span {
position: relative;
left: 0px;
}
.mdc-data-table__header-cell.sortable {
cursor: pointer;
}
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon)
span {
position: relative;
left: -24px;
}
.mdc-data-table__header-cell.not-sorted > * {
.mdc-data-table__header-cell > * {
transition: left 0.2s ease 0s;
}
.mdc-data-table__header-cell ha-icon {
top: 15px;
position: absolute;
}
.mdc-data-table__header-cell.not-sorted ha-icon {
left: -36px;
left: -20px;
}
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon):hover
.mdc-data-table__header-cell:not(.not-sorted) span,
.mdc-data-table__header-cell.not-sorted:hover span {
left: 24px;
}
.mdc-data-table__header-cell.mdc-data-table__header-cell--numeric:not(.not-sorted)
span,
.mdc-data-table__header-cell.mdc-data-table__header-cell--numeric.not-sorted:hover
span {
left: 0px;
left: 12px;
}
.mdc-data-table__header-cell:not(.not-sorted) ha-icon,
.mdc-data-table__header-cell:hover.not-sorted ha-icon {
left: 0px;
left: 12px;
}
.table-header {
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
search-input {
position: relative;
top: 2px;
}
.scroller {
overflow: auto;
}
slot[name="header"] {
display: block;
}
`;
}
}

View File

@@ -23,6 +23,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
DeviceEntityLookup,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
@@ -30,7 +31,6 @@ import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
import {
EntityRegistryEntry,
subscribeEntityRegistry,

View File

@@ -22,6 +22,7 @@ import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
computeDeviceName,
DeviceEntityLookup,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
@@ -29,7 +30,6 @@ import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
import {
EntityRegistryEntry,
subscribeEntityRegistry,

View File

@@ -6,7 +6,7 @@ import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { timeOut } from "@polymer/polymer/lib/utils/async";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import formatTime from "../../common/datetime/format_time";
import { formatTime } from "../../common/datetime/format_time";
// eslint-disable-next-line no-unused-vars
/* global Chart moment Color */

View File

@@ -10,6 +10,9 @@ import {
import { HassEntity } from "home-assistant-js-websocket";
import hassAttributeUtil from "../util/hass-attributes-util";
import { until } from "lit-html/directives/until";
let jsYamlPromise: Promise<typeof import("js-yaml")>;
@customElement("ha-attributes")
class HaAttributes extends LitElement {
@@ -32,7 +35,7 @@ class HaAttributes extends LitElement {
<div class="data-entry">
<div class="key">${attribute.replace(/_/g, " ")}</div>
<div class="value">
${this.formatAttributeValue(attribute)}
${this.formatAttribute(attribute)}
</div>
</div>
`
@@ -63,6 +66,10 @@ class HaAttributes extends LitElement {
color: var(--secondary-text-color);
text-align: right;
}
pre {
font-family: inherit;
font-size: inherit;
}
`;
}
@@ -75,18 +82,31 @@ class HaAttributes extends LitElement {
});
}
private formatAttributeValue(attribute: string): string {
private formatAttribute(attribute: string): string | TemplateResult {
if (!this.stateObj) {
return "-";
}
const value = this.stateObj.attributes[attribute];
return this.formatAttributeValue(value);
}
private formatAttributeValue(value: any): string | TemplateResult {
if (value === null) {
return "-";
}
if (Array.isArray(value)) {
return value.join(", ");
if (
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
(!Array.isArray(value) && value instanceof Object)
) {
if (!jsYamlPromise) {
jsYamlPromise = import(/* webpackChunkName: "js-yaml" */ "js-yaml");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value));
return html`
<pre>${until(yaml, "")}</pre>
`;
}
return value instanceof Object ? JSON.stringify(value, null, 2) : value;
return Array.isArray(value) ? value.join(", ") : value;
}
}

View File

@@ -9,7 +9,7 @@ const MwcCheckbox = customElements.get("mwc-checkbox") as Constructor<Checkbox>;
@customElement("ha-checkbox")
export class HaCheckbox extends MwcCheckbox {
protected firstUpdated() {
public firstUpdated() {
super.firstUpdated();
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
}

View File

@@ -1,12 +1,23 @@
import { customElement, CSSResult, css } from "lit-element";
import { customElement, CSSResult, css, html } from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import "@material/mwc-dialog";
import { style } from "@material/mwc-dialog/mwc-dialog-css";
// tslint:disable-next-line
import { Dialog } from "@material/mwc-dialog";
import { Constructor } from "../types";
import { Constructor, HomeAssistant } from "../types";
// tslint:disable-next-line
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
${title}
<paper-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")}
icon="hass:close"
dialogAction="close"
class="close_button"
></paper-icon-button>
`;
@customElement("ha-dialog")
export class HaDialog extends MwcDialog {
protected static get styles(): CSSResult[] {
@@ -19,6 +30,15 @@ export class HaDialog extends MwcDialog {
.mdc-dialog__container {
align-items: var(--vertial-align-dialog, center);
}
.mdc-dialog__title::before {
display: block;
height: 20px;
}
.close_button {
position: absolute;
right: 16px;
top: 12px;
}
`,
];
}

View File

@@ -0,0 +1,158 @@
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-menu-button/paper-menu-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-ripple/paper-ripple";
import {
customElement,
html,
LitElement,
property,
query,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import {
HaFormElement,
HaFormMultiSelectData,
HaFormMultiSelectSchema,
} from "./ha-form";
@customElement("ha-form-multi_select")
export class HaFormMultiSelect extends LitElement implements HaFormElement {
@property() public schema!: HaFormMultiSelectSchema;
@property() public data!: HaFormMultiSelectData;
@property() public label!: string;
@property() public suffix!: string;
@property() private _init = false;
@query("paper-menu-button") private _input?: HTMLElement;
public focus(): void {
if (this._input) {
this._input.focus();
}
}
protected render(): TemplateResult {
const options = Array.isArray(this.schema.options)
? this.schema.options
: Object.entries(this.schema.options!);
const data = this.data || [];
return html`
<paper-menu-button horizontal-align="right" vertical-offset="8">
<div class="dropdown-trigger" slot="dropdown-trigger">
<paper-ripple></paper-ripple>
<paper-input
id="input"
type="text"
readonly
value=${data
.map((value) => this.schema.options![value] || value)
.join(", ")}
label=${this.label}
input-role="button"
input-aria-haspopup="listbox"
autocomplete="off"
>
<iron-icon
icon="paper-dropdown-menu:arrow-drop-down"
suffix
slot="suffix"
></iron-icon>
</paper-input>
</div>
<paper-listbox
multi
slot="dropdown-content"
attr-for-selected="item-value"
.selectedValues=${data}
@selected-items-changed=${this._valueChanged}
@iron-select=${this._onSelect}
>
${// TS doesn't work with union array types https://github.com/microsoft/TypeScript/issues/36390
// @ts-ignore
options.map((item: string | [string, string]) => {
const value = this._optionValue(item);
return html`
<paper-icon-item .itemValue=${value}>
<paper-checkbox
.checked=${data.includes(value)}
slot="item-icon"
></paper-checkbox>
${this._optionLabel(item)}
</paper-icon-item>
`;
})}
</paper-listbox>
</paper-menu-button>
`;
}
protected firstUpdated() {
this.updateComplete.then(() => {
const input = (this.shadowRoot?.querySelector("paper-input")
?.inputElement as any)?.inputElement;
if (input) {
input.style.textOverflow = "ellipsis";
}
});
}
private _optionValue(item: string | string[]): string {
return Array.isArray(item) ? item[0] : item;
}
private _optionLabel(item: string | string[]): string {
return Array.isArray(item) ? item[1] || item[0] : item;
}
private _onSelect(ev: Event) {
ev.stopPropagation();
}
private _valueChanged(ev: CustomEvent): void {
if (!ev.detail.value || !this._init) {
// ignore first call because that is the init of the component
this._init = true;
return;
}
fireEvent(
this,
"value-changed",
{
value: ev.detail.value.map((element) => element.itemValue),
},
{ bubbles: false }
);
}
static get styles(): CSSResult {
return css`
paper-menu-button {
display: block;
padding: 0;
--paper-item-icon-width: 34px;
}
paper-ripple {
top: 12px;
left: 0px;
bottom: 8px;
right: 0px;
}
paper-input {
text-overflow: ellipsis;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-multi_select": HaFormMultiSelect;
}
}

View File

@@ -5,6 +5,8 @@ import {
property,
TemplateResult,
query,
CSSResult,
css,
} from "lit-element";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
import { fireEvent } from "../../common/dom/fire_event";
@@ -36,8 +38,10 @@ export class HaFormSelect extends LitElement implements HaFormElement {
.selected=${this.data}
@selected-item-changed=${this._valueChanged}
>
${this.schema.options!.map(
(item) => html`
${// TS doesn't work with union array types https://github.com/microsoft/TypeScript/issues/36390
// @ts-ignore
this.schema.options!.map(
(item: string | [string, string]) => html`
<paper-item .itemValue=${this._optionValue(item)}>
${this._optionLabel(item)}
</paper-item>
@@ -48,12 +52,12 @@ export class HaFormSelect extends LitElement implements HaFormElement {
`;
}
private _optionValue(item) {
private _optionValue(item: string | [string, string]) {
return Array.isArray(item) ? item[0] : item;
}
private _optionLabel(item) {
return Array.isArray(item) ? item[1] : item;
private _optionLabel(item: string | [string, string]) {
return Array.isArray(item) ? item[1] || item[0] : item;
}
private _valueChanged(ev: CustomEvent) {
@@ -64,6 +68,14 @@ export class HaFormSelect extends LitElement implements HaFormElement {
value: ev.detail.value.itemValue,
});
}
static get styles(): CSSResult {
return css`
paper-dropdown-menu {
display: block;
}
`;
}
}
declare global {

View File

@@ -12,6 +12,7 @@ import "./ha-form-integer";
import "./ha-form-float";
import "./ha-form-boolean";
import "./ha-form-select";
import "./ha-form-multi_select";
import "./ha-form-positive_time_period_dict";
import { fireEvent } from "../../common/dom/fire_event";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
@@ -22,6 +23,7 @@ export type HaFormSchema =
| HaFormFloatSchema
| HaFormBooleanSchema
| HaFormSelectSchema
| HaFormMultiSelectSchema
| HaFormTimeSchema;
export interface HaFormBaseSchema {
@@ -41,7 +43,12 @@ export interface HaFormIntegerSchema extends HaFormBaseSchema {
export interface HaFormSelectSchema extends HaFormBaseSchema {
type: "select";
options?: string[];
options?: string[] | Array<[string, string]>;
}
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
type: "multi_select";
options?: { [key: string]: string } | string[] | Array<[string, string]>;
}
export interface HaFormFloatSchema extends HaFormBaseSchema {
@@ -71,6 +78,7 @@ export type HaFormData =
| HaFormFloatData
| HaFormBooleanData
| HaFormSelectData
| HaFormMultiSelectData
| HaFormTimeData;
export type HaFormStringData = string;
@@ -78,6 +86,7 @@ export type HaFormIntegerData = number;
export type HaFormFloatData = number;
export type HaFormBooleanData = boolean;
export type HaFormSelectData = string;
export type HaFormMultiSelectData = string[];
export interface HaFormTimeData {
hours?: number;
minutes?: number;

View File

@@ -0,0 +1,65 @@
import {
html,
css,
LitElement,
TemplateResult,
property,
customElement,
} from "lit-element";
import "@polymer/paper-input/paper-input";
import "./ha-icon";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-icon-input")
export class HaIconInput extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public placeholder?: string;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public disabled = false;
protected render(): TemplateResult {
return html`
<paper-input
.value=${this.value}
.label=${this.label}
.placeholder=${this.placeholder}
@value-changed=${this._valueChanged}
.disabled=${this.disabled}
auto-validate
.errorMessage=${this.errorMessage}
pattern="^\\S+:\\S+$"
>
${this.value || this.placeholder
? html`
<ha-icon .icon=${this.value || this.placeholder} slot="suffix">
</ha-icon>
`
: ""}
</paper-input>
`;
}
private _valueChanged(ev: CustomEvent) {
this.value = ev.detail.value;
fireEvent(
this,
"value-changed",
{ value: ev.detail.value },
{
bubbles: false,
composed: false,
}
);
}
static get styles() {
return css`
ha-icon {
position: relative;
bottom: 4px;
}
`;
}
}

View File

@@ -68,6 +68,11 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
if (!this._related) {
return html``;
}
if (Object.keys(this._related).length === 0) {
return html`
${this.hass.localize("ui.components.related-items.no_related_found")}
`;
}
return html`
${this._related.config_entry && this._entries
? this._related.config_entry.map((relatedConfigEntryId) => {

View File

@@ -46,7 +46,18 @@ const SORT_VALUE_URL_PATHS = {
config: 11,
};
const panelSorter = (a, b) => {
const panelSorter = (a: PanelInfo, b: PanelInfo) => {
// Put all the Lovelace at the top.
const aLovelace = a.component_name === "lovelace";
const bLovelace = b.component_name === "lovelace";
if (aLovelace && !bLovelace) {
return -1;
}
if (bLovelace) {
return 1;
}
const aBuiltIn = a.url_path in SORT_VALUE_URL_PATHS;
const bBuiltIn = b.url_path in SORT_VALUE_URL_PATHS;
@@ -597,6 +608,7 @@ class HaSidebar extends LitElement {
paper-icon-item .item-text {
display: none;
max-width: calc(100% - 56px);
}
:host([expanded]) paper-icon-item .item-text {
display: block;

View File

@@ -6,6 +6,13 @@ import { afterNextRender } from "../common/util/render-status";
// tslint:disable-next-line
import { HaCodeEditor } from "./ha-code-editor";
declare global {
// for fire event
interface HASSDomEvents {
"editor-refreshed": undefined;
}
}
const isEmpty = (obj: object) => {
if (typeof obj !== "object") {
return false;
@@ -37,6 +44,7 @@ export class HaYamlEditor extends LitElement {
if (this._editor?.codemirror) {
this._editor.codemirror.refresh();
}
afterNextRender(() => fireEvent(this, "editor-refreshed"));
});
}

View File

@@ -41,7 +41,8 @@ export interface MarkerLocation {
id: string;
icon?: string;
radius_color?: string;
editable?: boolean;
location_editable?: boolean;
radius_editable?: boolean;
}
@customElement("ha-locations-editor")
@@ -208,7 +209,7 @@ export class HaLocationsEditor extends LitElement {
}
);
circle.addTo(this._leafletMap!);
if (location.editable) {
if (location.radius_editable || location.location_editable) {
// @ts-ignore
circle.editing.enable();
// @ts-ignore
@@ -230,19 +231,25 @@ export class HaLocationsEditor extends LitElement {
// @ts-ignore
(ev: MouseEvent) => this._markerClicked(ev)
);
resizeMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._updateRadius(ev)
);
if (location.radius_editable) {
resizeMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._updateRadius(ev)
);
} else {
resizeMarker.remove();
}
this._locationMarkers![location.id] = circle;
} else {
this._circles[location.id] = circle;
}
}
if (!location.radius || !location.editable) {
if (
!location.radius ||
(!location.radius_editable && !location.location_editable)
) {
const options: MarkerOptions = {
draggable: Boolean(location.editable),
title: location.name,
};

View File

@@ -0,0 +1,311 @@
import "@polymer/paper-icon-button/paper-icon-button";
import { Circle, Layer, Map, Marker } from "leaflet";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import {
LeafletModuleType,
setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { debounce } from "../../common/util/debounce";
import { HomeAssistant } from "../../types";
import "../../panels/map/ha-entity-marker";
@customElement("ha-map")
class HaMap extends LitElement {
@property() public hass?: HomeAssistant;
@property() public entities?: string[];
@property() public darkMode = false;
@property() public zoom?: number;
// tslint:disable-next-line
private Leaflet?: LeafletModuleType;
private _leafletMap?: Map;
// @ts-ignore
private _resizeObserver?: ResizeObserver;
private _debouncedResizeListener = debounce(
() => {
if (!this._leafletMap) {
return;
}
this._leafletMap.invalidateSize();
},
100,
false
);
private _mapItems: Array<Marker | Circle> = [];
private _mapZones: Array<Marker | Circle> = [];
private _connected = false;
public connectedCallback(): void {
super.connectedCallback();
this._connected = true;
if (this.hasUpdated) {
this.loadMap();
this._attachObserver();
}
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._connected = false;
if (this._leafletMap) {
this._leafletMap.remove();
this._leafletMap = undefined;
this.Leaflet = undefined;
}
if (this._resizeObserver) {
this._resizeObserver.unobserve(this._mapEl);
} else {
window.removeEventListener("resize", this._debouncedResizeListener);
}
}
protected render(): TemplateResult {
if (!this.entities) {
return html``;
}
return html`
<div id="map"></div>
`;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this.loadMap();
if (this._connected) {
this._attachObserver();
}
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("hass")) {
this._drawEntities();
this._fitMap();
}
}
private get _mapEl(): HTMLDivElement {
return this.shadowRoot!.getElementById("map") as HTMLDivElement;
}
private async loadMap(): Promise<void> {
[this._leafletMap, this.Leaflet] = await setupLeafletMap(
this._mapEl,
this.darkMode
);
this._drawEntities();
this._leafletMap.invalidateSize();
this._fitMap();
}
private _fitMap(): void {
if (!this._leafletMap || !this.Leaflet || !this.hass) {
return;
}
if (this._mapItems.length === 0) {
this._leafletMap.setView(
new this.Leaflet.LatLng(
this.hass.config.latitude,
this.hass.config.longitude
),
this.zoom || 14
);
return;
}
const bounds = this.Leaflet.latLngBounds(
this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : []
);
this._leafletMap.fitBounds(bounds.pad(0.5));
if (this.zoom && this._leafletMap.getZoom() > this.zoom) {
this._leafletMap.setZoom(this.zoom);
}
}
private _drawEntities(): void {
const hass = this.hass;
const map = this._leafletMap;
const Leaflet = this.Leaflet;
if (!hass || !map || !Leaflet) {
return;
}
if (this._mapItems) {
this._mapItems.forEach((marker) => marker.remove());
}
const mapItems: Layer[] = (this._mapItems = []);
if (this._mapZones) {
this._mapZones.forEach((marker) => marker.remove());
}
const mapZones: Layer[] = (this._mapZones = []);
const allEntities = this.entities!.concat();
for (const entity of allEntities) {
const entityId = entity;
const stateObj = hass.states[entityId];
if (!stateObj) {
continue;
}
const title = computeStateName(stateObj);
const {
latitude,
longitude,
passive,
icon,
radius,
entity_picture: entityPicture,
gps_accuracy: gpsAccuracy,
} = stateObj.attributes;
if (!(latitude && longitude)) {
continue;
}
if (computeStateDomain(stateObj) === "zone") {
// DRAW ZONE
if (passive) {
continue;
}
// create icon
let iconHTML = "";
if (icon) {
const el = document.createElement("ha-icon");
el.setAttribute("icon", icon);
iconHTML = el.outerHTML;
} else {
const el = document.createElement("span");
el.innerHTML = title;
iconHTML = el.outerHTML;
}
// create marker with the icon
mapZones.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: this.darkMode ? "dark" : "light",
}),
interactive: false,
title,
})
);
// create circle around it
mapZones.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#FF9800",
radius,
})
);
continue;
}
// DRAW ENTITY
// create icon
const entityName = title
.split(" ")
.map((part) => part[0])
.join("")
.substr(0, 3);
// create market with the icon
mapItems.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
// Leaflet clones this element before adding it to the map. This messes up
// our Polymer object and we can't pass data through. Thus we hack like this.
html: `
<ha-entity-marker
entity-id="${entityId}"
entity-name="${entityName}"
entity-picture="${entityPicture || ""}"
></ha-entity-marker>
`,
iconSize: [48, 48],
className: "",
}),
title: computeStateName(stateObj),
})
);
// create circle around if entity has accuracy
if (gpsAccuracy) {
mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#0288D1",
radius: gpsAccuracy,
})
);
}
}
this._mapItems.forEach((marker) => map.addLayer(marker));
this._mapZones.forEach((marker) => map.addLayer(marker));
}
private _attachObserver(): void {
// Observe changes to map size and invalidate to prevent broken rendering
// Uses ResizeObserver in Chrome, otherwise window resize event
// @ts-ignore
if (typeof ResizeObserver === "function") {
// @ts-ignore
this._resizeObserver = new ResizeObserver(() =>
this._debouncedResizeListener()
);
this._resizeObserver.observe(this._mapEl);
} else {
window.addEventListener("resize", this._debouncedResizeListener);
}
}
static get styles(): CSSResult {
return css`
:host {
display: block;
height: 300px;
}
#map {
height: 100%;
}
#map.dark {
background: #090909;
}
.dark {
color: #ffffff;
}
.light {
color: #000000;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-map": HaMap;
}
}

View File

@@ -5,7 +5,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./entity/ha-chart-base";
import LocalizeMixin from "../mixins/localize-mixin";
import formatDateTime from "../common/datetime/format_date_time";
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
static get template() {
@@ -317,7 +317,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
const item = items[0];
const date = data.datasets[item.datasetIndex].data[item.index].x;
return formatDateTime(date, this.hass.language);
return formatDateTimeWithSeconds(date, this.hass.language);
};
const chartOptions = {

View File

@@ -6,7 +6,7 @@ import LocalizeMixin from "../mixins/localize-mixin";
import "./entity/ha-chart-base";
import formatDateTime from "../common/datetime/format_date_time";
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
import { computeRTL } from "../common/util/compute_rtl";
class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
@@ -165,8 +165,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
const formatTooltipLabel = (item, data) => {
const values = data.datasets[item.datasetIndex].data[item.index];
const start = formatDateTime(values[0], this.hass.language);
const end = formatDateTime(values[1], this.hass.language);
const start = formatDateTimeWithSeconds(values[0], this.hass.language);
const end = formatDateTimeWithSeconds(values[1], this.hass.language);
const state = values[2];
return [state, start, end];

View File

@@ -173,6 +173,12 @@ export type Condition =
| DeviceCondition
| LogicalCondition;
export const triggerAutomation = (hass: HomeAssistant, entityId: string) => {
hass.callService("automation", "trigger", {
entity_id: entityId,
});
};
export const deleteAutomation = (hass: HomeAssistant, id: string) =>
hass.callApi("DELETE", `config/automation/config/${id}`);

View File

@@ -9,7 +9,7 @@ import { HomeAssistant } from "../types";
import { HassEntity } from "home-assistant-js-websocket";
import { LocalizeFunc } from "../common/translations/localize";
interface CacheConfig {
export interface CacheConfig {
refresh: number;
cacheKey: string;
hoursToShow: number;

View File

@@ -1,3 +1,5 @@
import { HaFormSchema } from "../components/ha-form/ha-form";
export interface DataEntryFlowProgressedEvent {
type: "data_entry_flow_progressed";
data: {
@@ -7,12 +9,6 @@ export interface DataEntryFlowProgressedEvent {
};
}
export interface FieldSchema {
name: string;
default?: any;
optional: boolean;
}
export interface DataEntryFlowProgress {
flow_id: string;
handler: string;
@@ -27,7 +23,7 @@ export interface DataEntryFlowStepForm {
flow_id: string;
handler: string;
step_id: string;
data_schema: FieldSchema[];
data_schema: HaFormSchema[];
errors: { [key: string]: string };
description_placeholders: { [key: string]: string };
}

View File

@@ -99,49 +99,65 @@ export const deviceAutomationsEqual = (
export const localizeDeviceAutomationAction = (
hass: HomeAssistant,
action: DeviceAction
) => {
): string => {
const state = action.entity_id ? hass.states[action.entity_id] : undefined;
return hass.localize(
`component.${action.domain}.device_automation.action_type.${action.type}`,
"entity_name",
state ? computeStateName(state) : "<unknown>",
"subtype",
return (
hass.localize(
`component.${action.domain}.device_automation.action_subtype.${action.subtype}`
)
`component.${action.domain}.device_automation.action_type.${action.type}`,
"entity_name",
state ? computeStateName(state) : action.entity_id || "<unknown>",
"subtype",
action.subtype
? hass.localize(
`component.${action.domain}.device_automation.action_subtype.${action.subtype}`
) || action.subtype
: ""
) || (action.subtype ? `"${action.subtype}" ${action.type}` : action.type!)
);
};
export const localizeDeviceAutomationCondition = (
hass: HomeAssistant,
condition: DeviceCondition
) => {
): string => {
const state = condition.entity_id
? hass.states[condition.entity_id]
: undefined;
return hass.localize(
`component.${condition.domain}.device_automation.condition_type.${condition.type}`,
"entity_name",
state ? computeStateName(state) : "<unknown>",
"subtype",
return (
hass.localize(
`component.${condition.domain}.device_automation.condition_subtype.${condition.subtype}`
)
`component.${condition.domain}.device_automation.condition_type.${condition.type}`,
"entity_name",
state ? computeStateName(state) : condition.entity_id || "<unknown>",
"subtype",
condition.subtype
? hass.localize(
`component.${condition.domain}.device_automation.condition_subtype.${condition.subtype}`
) || condition.subtype
: ""
) ||
(condition.subtype
? `"${condition.subtype}" ${condition.type}`
: condition.type!)
);
};
export const localizeDeviceAutomationTrigger = (
hass: HomeAssistant,
trigger: DeviceTrigger
) => {
): string => {
const state = trigger.entity_id ? hass.states[trigger.entity_id] : undefined;
return hass.localize(
`component.${trigger.domain}.device_automation.trigger_type.${trigger.type}`,
"entity_name",
state ? computeStateName(state) : "<unknown>",
"subtype",
return (
hass.localize(
`component.${trigger.domain}.device_automation.trigger_subtype.${trigger.subtype}`
)
`component.${trigger.domain}.device_automation.trigger_type.${trigger.type}`,
"entity_name",
state ? computeStateName(state) : trigger.entity_id || "<unknown>",
"subtype",
trigger.subtype
? hass.localize(
`component.${trigger.domain}.device_automation.trigger_subtype.${trigger.subtype}`
) || trigger.subtype
: ""
) ||
(trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!)
);
};

View File

@@ -17,6 +17,10 @@ export interface DeviceRegistryEntry {
name_by_user?: string;
}
export interface DeviceEntityLookup {
[deviceId: string]: EntityRegistryEntry[];
}
export interface DeviceRegistryEntryMutableParams {
area_id?: string | null;
name_by_user?: string | null;

View File

@@ -6,14 +6,23 @@ import { debounce } from "../common/util/debounce";
export interface EntityRegistryEntry {
entity_id: string;
name: string;
icon?: string;
platform: string;
config_entry_id?: string;
device_id?: string;
disabled_by: string | null;
}
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
unique_id: string;
capabilities: object;
original_name?: string;
original_icon?: string;
}
export interface EntityRegistryEntryUpdateParams {
name?: string | null;
icon?: string | null;
disabled_by?: string | null;
new_entity_id?: string;
}
@@ -29,12 +38,21 @@ export const computeEntityRegistryName = (
return state ? computeStateName(state) : null;
};
export const getExtendedEntityRegistryEntry = (
hass: HomeAssistant,
entityId: string
): Promise<ExtEntityRegistryEntry> =>
hass.callWS({
type: "config/entity_registry/get",
entity_id: entityId,
});
export const updateEntityRegistryEntry = (
hass: HomeAssistant,
entityId: string,
updates: Partial<EntityRegistryEntryUpdateParams>
): Promise<EntityRegistryEntry> =>
hass.callWS<EntityRegistryEntry>({
): Promise<ExtEntityRegistryEntry> =>
hass.callWS({
type: "config/entity_registry/update",
entity_id: entityId,
...updates,

4
src/data/external.ts Normal file
View File

@@ -0,0 +1,4 @@
export const isExternal =
window.externalApp ||
window.webkit?.messageHandlers?.getExternalAuth ||
location.search.includes("external_auth=1");

View File

@@ -59,3 +59,12 @@ export const getOptimisticFrontendUserDataCollection = <
`_frontendUserData-${userDataKey}`,
() => fetchFrontendUserData(conn, userDataKey)
);
export const subscribeFrontendUserData = <UserDataKey extends ValidUserDataKey>(
conn: Connection,
userDataKey: UserDataKey,
onChange: (state: FrontendUserData[UserDataKey] | null) => void
) =>
getOptimisticFrontendUserDataCollection(conn, userDataKey).subscribe(
onChange
);

View File

@@ -1,11 +0,0 @@
import { HomeAssistant } from "../types";
export const setInputSelectOption = (
hass: HomeAssistant,
entity: string,
option: string
) =>
hass.callService("input_select", "select_option", {
option,
entity_id: entity,
});

43
src/data/input_boolean.ts Normal file
View File

@@ -0,0 +1,43 @@
import { HomeAssistant } from "../types";
export interface InputBoolean {
id: string;
name: string;
icon?: string;
initial?: boolean;
}
export interface InputBooleanMutableParams {
name: string;
icon: string;
initial: boolean;
}
export const fetchInputBoolean = (hass: HomeAssistant) =>
hass.callWS<InputBoolean[]>({ type: "input_boolean/list" });
export const createInputBoolean = (
hass: HomeAssistant,
values: InputBooleanMutableParams
) =>
hass.callWS<InputBoolean>({
type: "input_boolean/create",
...values,
});
export const updateInputBoolean = (
hass: HomeAssistant,
id: string,
updates: Partial<InputBooleanMutableParams>
) =>
hass.callWS<InputBoolean>({
type: "input_boolean/update",
input_boolean_id: id,
...updates,
});
export const deleteInputBoolean = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "input_boolean/delete",
input_boolean_id: id,
});

View File

@@ -1,5 +1,22 @@
import { HomeAssistant } from "../types";
export interface InputDateTime {
id: string;
name: string;
icon?: string;
initial?: string;
has_time: boolean;
has_date: boolean;
}
export interface InputDateTimeMutableParams {
name: string;
icon: string;
initial: string;
has_time: boolean;
has_date: boolean;
}
export const setInputDateTimeValue = (
hass: HomeAssistant,
entityId: string,
@@ -9,3 +26,32 @@ export const setInputDateTimeValue = (
const param = { entity_id: entityId, time, date };
hass.callService(entityId.split(".", 1)[0], "set_datetime", param);
};
export const fetchInputDateTime = (hass: HomeAssistant) =>
hass.callWS<InputDateTime[]>({ type: "input_datetime/list" });
export const createInputDateTime = (
hass: HomeAssistant,
values: InputDateTimeMutableParams
) =>
hass.callWS<InputDateTime>({
type: "input_datetime/create",
...values,
});
export const updateInputDateTime = (
hass: HomeAssistant,
id: string,
updates: Partial<InputDateTimeMutableParams>
) =>
hass.callWS<InputDateTime>({
type: "input_datetime/update",
input_datetime_id: id,
...updates,
});
export const deleteInputDateTime = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "input_datetime/delete",
input_datetime_id: id,
});

53
src/data/input_number.ts Normal file
View File

@@ -0,0 +1,53 @@
import { HomeAssistant } from "../types";
export interface InputNumber {
id: string;
name: string;
min: number;
max: number;
icon?: string;
initial?: number;
step?: number;
mode?: "box" | "slider";
unit_of_measurement?: string;
}
export interface InputNumberMutableParams {
name: string;
icon: string;
initial: number;
min: number;
max: number;
step: number;
mode: "box" | "slider";
unit_of_measurement?: string;
}
export const fetchInputNumber = (hass: HomeAssistant) =>
hass.callWS<InputNumber[]>({ type: "input_number/list" });
export const createInputNumber = (
hass: HomeAssistant,
values: InputNumberMutableParams
) =>
hass.callWS<InputNumber>({
type: "input_number/create",
...values,
});
export const updateInputNumber = (
hass: HomeAssistant,
id: string,
updates: Partial<InputNumberMutableParams>
) =>
hass.callWS<InputNumber>({
type: "input_number/update",
input_number_id: id,
...updates,
});
export const deleteInputNumber = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "input_number/delete",
input_number_id: id,
});

55
src/data/input_select.ts Normal file
View File

@@ -0,0 +1,55 @@
import { HomeAssistant } from "../types";
export interface InputSelect {
id: string;
name: string;
options: string[];
icon?: string;
initial?: string;
}
export interface InputSelectMutableParams {
name: string;
icon: string;
initial: string;
options: string[];
}
export const setInputSelectOption = (
hass: HomeAssistant,
entity: string,
option: string
) =>
hass.callService("input_select", "select_option", {
option,
entity_id: entity,
});
export const fetchInputSelect = (hass: HomeAssistant) =>
hass.callWS<InputSelect[]>({ type: "input_select/list" });
export const createInputSelect = (
hass: HomeAssistant,
values: InputSelectMutableParams
) =>
hass.callWS<InputSelect>({
type: "input_select/create",
...values,
});
export const updateInputSelect = (
hass: HomeAssistant,
id: string,
updates: Partial<InputSelectMutableParams>
) =>
hass.callWS<InputSelect>({
type: "input_select/update",
input_select_id: id,
...updates,
});
export const deleteInputSelect = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "input_select/delete",
input_select_id: id,
});

View File

@@ -1,7 +1,57 @@
import { HomeAssistant } from "../types";
export interface InputText {
id: string;
name: string;
icon?: string;
initial?: string;
min?: number;
max?: number;
pattern?: string;
mode?: "text" | "password";
}
export interface InputTextMutableParams {
name: string;
icon: string;
initial: string;
min: number;
max: number;
pattern: string;
mode: "text" | "password";
}
export const setValue = (hass: HomeAssistant, entity: string, value: string) =>
hass.callService(entity.split(".", 1)[0], "set_value", {
value,
entity_id: entity,
});
export const fetchInputText = (hass: HomeAssistant) =>
hass.callWS<InputText[]>({ type: "input_text/list" });
export const createInputText = (
hass: HomeAssistant,
values: InputTextMutableParams
) =>
hass.callWS<InputText>({
type: "input_text/create",
...values,
});
export const updateInputText = (
hass: HomeAssistant,
id: string,
updates: Partial<InputTextMutableParams>
) =>
hass.callWS<InputText>({
type: "input_text/update",
input_text_id: id,
...updates,
});
export const deleteInputText = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "input_text/delete",
input_text_id: id,
});

10
src/data/integration.ts Normal file
View File

@@ -0,0 +1,10 @@
import { LocalizeFunc } from "../common/translations/localize";
export const integrationDocsUrl = (domain: string) =>
`https://www.home-assistant.io/integrations/${domain}`;
export const integrationIssuesUrl = (domain: string) =>
`https://github.com/home-assistant/home-assistant/issues?q=is%3Aissue+is%3Aopen+label%3A%22integration%3A+${domain}%22`;
export const domainToName = (localize: LocalizeFunc, domain: string) =>
localize(`domain.${domain}`) || domain;

View File

@@ -1,12 +1,57 @@
import { HomeAssistant } from "../types";
import { Connection, getCollection } from "home-assistant-js-websocket";
import {
Connection,
getCollection,
HassEventBase,
} from "home-assistant-js-websocket";
import { HASSDomEvent } from "../common/dom/fire_event";
export interface LovelaceConfig {
title?: string;
views: LovelaceViewConfig[];
background?: string;
resources?: Array<{ type: "css" | "js" | "module" | "html"; url: string }>;
}
export interface LovelaceResource {
id: string;
type: "css" | "js" | "module" | "html";
url: string;
}
export interface LovelaceResourcesMutableParams {
res_type: "css" | "js" | "module" | "html";
url: string;
}
export type LovelaceDashboard =
| LovelaceYamlDashboard
| LovelaceStorageDashboard;
interface LovelaceGenericDashboard {
id: string;
url_path: string;
require_admin: boolean;
sidebar?: { icon: string; title: string };
}
export interface LovelaceYamlDashboard extends LovelaceGenericDashboard {
mode: "yaml";
filename: string;
}
export interface LovelaceStorageDashboard extends LovelaceGenericDashboard {
mode: "storage";
}
export interface LovelaceDashboardMutableParams {
require_admin: boolean;
sidebar: { icon: string; title: string } | null;
}
export interface LovelaceDashboardCreateParams
extends LovelaceDashboardMutableParams {
url_path: string;
mode: "storage";
}
export interface LovelaceViewConfig {
@@ -95,47 +140,139 @@ export type ActionConfig =
| NoActionConfig
| CustomActionConfig;
type LovelaceUpdatedEvent = HassEventBase & {
event_type: "lovelace_updated";
data: {
url_path: string | null;
mode: "yaml" | "storage";
};
};
export const fetchResources = (conn: Connection): Promise<LovelaceResource[]> =>
conn.sendMessagePromise({
type: "lovelace/resources",
});
export const createResource = (
hass: HomeAssistant,
values: LovelaceResourcesMutableParams
) =>
hass.callWS<LovelaceResource>({
type: "lovelace/resources/create",
...values,
});
export const updateResource = (
hass: HomeAssistant,
id: string,
updates: Partial<LovelaceResourcesMutableParams>
) =>
hass.callWS<LovelaceResource>({
type: "lovelace/resources/update",
resource_id: id,
...updates,
});
export const deleteResource = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "lovelace/resources/delete",
resource_id: id,
});
export const fetchDashboards = (
hass: HomeAssistant
): Promise<LovelaceDashboard[]> =>
hass.callWS({
type: "lovelace/dashboards/list",
});
export const createDashboard = (
hass: HomeAssistant,
values: LovelaceDashboardCreateParams
) =>
hass.callWS<LovelaceDashboard>({
type: "lovelace/dashboards/create",
...values,
});
export const updateDashboard = (
hass: HomeAssistant,
id: string,
updates: Partial<LovelaceDashboardMutableParams>
) =>
hass.callWS<LovelaceDashboard>({
type: "lovelace/dashboards/update",
dashboard_id: id,
...updates,
});
export const deleteDashboard = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "lovelace/dashboards/delete",
dashboard_id: id,
});
export const fetchConfig = (
conn: Connection,
urlPath: string | null,
force: boolean
): Promise<LovelaceConfig> =>
conn.sendMessagePromise({
type: "lovelace/config",
url_path: urlPath,
force,
});
export const saveConfig = (
hass: HomeAssistant,
urlPath: string | null,
config: LovelaceConfig
): Promise<void> =>
hass.callWS({
type: "lovelace/config/save",
url_path: urlPath,
config,
});
export const deleteConfig = (hass: HomeAssistant): Promise<void> =>
export const deleteConfig = (
hass: HomeAssistant,
urlPath: string | null
): Promise<void> =>
hass.callWS({
type: "lovelace/config/delete",
url_path: urlPath,
});
export const subscribeLovelaceUpdates = (
conn: Connection,
urlPath: string | null,
onChange: () => void
) => conn.subscribeEvents(onChange, "lovelace_updated");
) =>
conn.subscribeEvents<LovelaceUpdatedEvent>((ev) => {
if (ev.data.url_path === urlPath) {
onChange();
}
}, "lovelace_updated");
export const getLovelaceCollection = (conn: Connection) =>
export const getLovelaceCollection = (
conn: Connection,
urlPath: string | null = null
) =>
getCollection(
conn,
"_lovelace",
(conn2) => fetchConfig(conn2, false),
`_lovelace_${urlPath ?? ""}`,
(conn2) => fetchConfig(conn2, urlPath, false),
(_conn, store) =>
subscribeLovelaceUpdates(conn, () =>
fetchConfig(conn, false).then((config) => store.setState(config, true))
subscribeLovelaceUpdates(conn, urlPath, () =>
fetchConfig(conn, urlPath, false).then((config) =>
store.setState(config, true)
)
)
);
export interface WindowWithLovelaceProm extends Window {
llConfProm?: Promise<LovelaceConfig>;
llResProm?: Promise<LovelaceResource[]>;
}
export interface ActionHandlerOptions {

View File

@@ -3,8 +3,19 @@ import { HomeAssistant } from "../types";
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
export const SUPPORT_PAUSE = 1;
export const SUPPORT_SEEK = 2;
export const SUPPORT_VOLUME_SET = 4;
export const SUPPORT_VOLUME_MUTE = 8;
export const SUPPORT_PREVIOUS_TRACK = 16;
export const SUPPORT_NEXT_TRACK = 32;
export const SUPPORT_TURN_ON = 128;
export const SUPPORT_TURN_OFF = 256;
export const SUPPORT_PLAY_MEDIA = 512;
export const SUPPORT_VOLUME_BUTTONS = 1024;
export const SUPPORT_SELECT_SOURCE = 2048;
export const SUPPORT_STOP = 4096;
export const SUPPORTS_PLAY = 16384;
export const SUPPORT_SELECT_SOUND_MODE = 65536;
export const OFF_STATES = ["off", "idle"];
export interface MediaPlayerThumbnail {

2
src/data/sensor.ts Normal file
View File

@@ -0,0 +1,2 @@
export const SENSOR_DEVICE_CLASS_BATTERY = "battery";
export const SENSOR_DEVICE_CLASS_TIMESTAMP = "timestamp";

View File

@@ -1,6 +1,7 @@
import { HomeAssistant } from "../types";
export interface LoggedError {
name: string;
message: string;
level: string;
source: string;
@@ -14,3 +15,8 @@ export interface LoggedError {
export const fetchSystemLog = (hass: HomeAssistant) =>
hass.callApi<LoggedError[]>("GET", "error/all");
export const getLoggedErrorIntegration = (item: LoggedError) =>
item.name.startsWith("homeassistant.components.")
? item.name.split(".")[2]
: undefined;

22
src/data/vacuum.ts Normal file
View File

@@ -0,0 +1,22 @@
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
export const VACUUM_SUPPORT_PAUSE = 4;
export const VACUUM_SUPPORT_STOP = 8;
export const VACUUM_SUPPORT_RETURN_HOME = 16;
export const VACUUM_SUPPORT_FAN_SPEED = 32;
export const VACUUM_SUPPORT_BATTERY = 64;
export const VACUUM_SUPPORT_STATUS = 128;
export const VACUUM_SUPPORT_LOCATE = 512;
export const VACUUM_SUPPORT_CLEAN_SPOT = 1024;
export const VACUUM_SUPPORT_START = 8192;
export type VacuumEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
battery_level: number;
fan_speed: any;
[key: string]: any;
};
};

View File

@@ -8,7 +8,7 @@ const fetchThemes = (conn) =>
const subscribeUpdates = (conn, store) =>
conn.subscribeEvents(
(event) => store.setState(event.data, true),
() => fetchThemes(conn).then((data) => store.setState(data, true)),
"themes_updated"
);

View File

@@ -1,4 +1,5 @@
import { HomeAssistant } from "../types";
import { navigate } from "../common/navigate";
export const defaultRadiusColor = "#FF9800";
export const homeRadiusColor: string = "#03a9f4";
@@ -48,3 +49,19 @@ export const deleteZone = (hass: HomeAssistant, zoneId: string) =>
type: "zone/delete",
zone_id: zoneId,
});
let inititialZoneEditorData: Partial<ZoneMutableParams> | undefined;
export const showZoneEditor = (
el: HTMLElement,
data?: Partial<ZoneMutableParams>
) => {
inititialZoneEditorData = data;
navigate(el, "/config/zone/new");
};
export const getZoneEditorInitData = () => {
const data = inititialZoneEditorData;
inititialZoneEditorData = undefined;
return data;
};

View File

@@ -115,7 +115,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
.disabled=${this._submitting}
>
${this.hass.localize(
"ui.panel.config.entities.editor.update"
"ui.dialogs.config_entry_system_options.update"
)}
</mwc-button>
</div>

View File

@@ -7,8 +7,8 @@ import {
DataEntryFlowStepForm,
DataEntryFlowStep,
DataEntryFlowStepAbort,
FieldSchema,
} from "../../data/data_entry_flow";
import { HaFormSchema } from "../../components/ha-form/ha-form";
export interface FlowConfig {
loadDevicesAndAreas: boolean;
@@ -45,7 +45,7 @@ export interface FlowConfig {
renderShowFormStepFieldLabel(
hass: HomeAssistant,
step: DataEntryFlowStepForm,
field: FieldSchema
field: HaFormSchema
): string;
renderShowFormStepFieldError(

View File

@@ -47,7 +47,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepHeader(hass, step) {
return (
hass.localize(
`component.${step.handler}.options.step.${step.step_id}.title`
`component.${configEntry.domain}.options.step.${step.step_id}.title`
) || hass.localize(`ui.dialogs.options_flow.form.header`)
);
},
@@ -55,7 +55,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepDescription(hass, step) {
const description = localizeKey(
hass.localize,
`component.${step.handler}.config.step.${step.step_id}.description`,
`component.${configEntry.domain}.options.step.${step.step_id}.description`,
step.description_placeholders
);
return description

View File

@@ -18,8 +18,10 @@ import "../../resources/ha-style";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import { configFlowContentStyles } from "./styles";
import { DataEntryFlowStepForm, FieldSchema } from "../../data/data_entry_flow";
import { DataEntryFlowStepForm } from "../../data/data_entry_flow";
import { FlowConfig } from "./show-dialog-data-entry-flow";
// tslint:disable-next-line
import { HaFormSchema } from "../../components/ha-form/ha-form";
@customElement("step-flow-form")
class StepFlowForm extends LitElement {
@@ -176,7 +178,7 @@ class StepFlowForm extends LitElement {
this._stepData = ev.detail.value;
}
private _labelCallback = (field: FieldSchema): string =>
private _labelCallback = (field: HaFormSchema): string =>
this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field);
private _errorCallback = (error: string) =>

View File

@@ -20,6 +20,7 @@ import "../../common/search/search-input";
import { styleMap } from "lit-html/directives/style-map";
import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
import { classMap } from "lit-html/directives/class-map";
interface HandlerObj {
name: string;
@@ -39,7 +40,8 @@ class StepFlowPickHandler extends LitElement {
private _getHandlers = memoizeOne((h: string[], filter?: string) => {
const handlers: HandlerObj[] = h.map((handler) => {
return {
name: this.hass.localize(`component.${handler}.config.title`),
name:
this.hass.localize(`component.${handler}.config.title`) || handler,
slug: handler,
};
});
@@ -68,7 +70,10 @@ class StepFlowPickHandler extends LitElement {
.filter=${this.filter}
@value-changed=${this._filterChanged}
></search-input>
<div style=${styleMap({ width: `${this._width}px` })}>
<div
style=${styleMap({ width: `${this._width}px` })}
class=${classMap({ advanced: Boolean(this.showAdvanced) })}
>
${handlers.map(
(handler: HandlerObj) =>
html`
@@ -142,6 +147,14 @@ class StepFlowPickHandler extends LitElement {
overflow: auto;
max-height: 600px;
}
@media all and (max-height: 1px) {
div {
max-height: calc(100vh - 205px);
}
div.advanced {
max-height: calc(100vh - 300px);
}
}
paper-item {
cursor: pointer;
}

View File

@@ -129,6 +129,10 @@ class DialogBox extends LitElement {
return [
haStyleDialog,
css`
:host([inert]) {
pointer-events: initial !important;
cursor: initial !important;
}
ha-paper-dialog {
min-width: 400px;
max-width: 500px;

View File

@@ -8,7 +8,6 @@ import "../resources/ha-style";
import "./more-info/more-info-controls";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import DialogMixin from "../mixins/dialog-mixin";
@@ -80,8 +79,7 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
class="no-padding"
hass="[[hass]]"
state-obj="[[stateObj]]"
dialog-element="[[_dialogElement]]"
registry-entry="[[_registryInfo]]"
dialog-element="[[_dialogElement()]]"
large="{{large}}"
></more-info-controls>
`;
@@ -102,9 +100,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
observer: "_largeChanged",
},
_dialogElement: Object,
_registryInfo: Object,
dataDomain: {
computed: "_computeDomain(stateObj)",
reflectToAttribute: true,
@@ -116,9 +111,8 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
return ["_dialogOpenChanged(opened)"];
}
ready() {
super.ready();
this._dialogElement = this;
_dialogElement() {
return this;
}
_computeDomain(stateObj) {
@@ -129,11 +123,10 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
return hass.states[hass.moreInfoEntityId] || null;
}
async _stateObjChanged(newVal, oldVal) {
async _stateObjChanged(newVal) {
if (!newVal) {
this.setProperties({
opened: false,
_registryInfo: null,
large: false,
});
return;
@@ -146,25 +139,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
this.opened = true;
})
);
if (
!isComponentLoaded(this.hass, "config") ||
(oldVal && oldVal.entity_id === newVal.entity_id)
) {
return;
}
if (this.hass.user.is_admin) {
try {
const info = await this.hass.callWS({
type: "config/entity_registry/get",
entity_id: newVal.entity_id,
});
this._registryInfo = info;
} catch (err) {
this._registryInfo = null;
}
}
}
_dialogOpenChanged(newVal) {

View File

@@ -13,6 +13,7 @@ import "@material/mwc-button";
import "../../../components/ha-relative-time";
import { HomeAssistant } from "../../../types";
import { triggerAutomation } from "../../../data/automation";
@customElement("more-info-automation")
class MoreInfoAutomation extends LitElement {
@@ -42,9 +43,7 @@ class MoreInfoAutomation extends LitElement {
}
private handleAction() {
this.hass.callService("automation", "trigger", {
entity_id: this.stateObj!.entity_id,
});
triggerAutomation(this.hass, this.stateObj!.entity_id);
}
static get styles(): CSSResult {

View File

@@ -45,7 +45,7 @@ class MoreInfoCamera extends LitElement {
return html`
<ha-camera-stream
.hass="${this.hass}"
.hass=${this.hass}
.stateObj="${this.stateObj}"
showcontrols
></ha-camera-stream>

View File

@@ -100,7 +100,8 @@ class MoreInfoClimate extends LitElement {
</div>
`
: ""}
${stateObj.attributes.temperature !== undefined
${stateObj.attributes.temperature !== undefined &&
stateObj.attributes.temperature !== null
? html`
<ha-climate-control
.value=${stateObj.attributes.temperature}
@@ -112,8 +113,10 @@ class MoreInfoClimate extends LitElement {
></ha-climate-control>
`
: ""}
${stateObj.attributes.target_temp_low !== undefined ||
stateObj.attributes.target_temp_high !== undefined
${(stateObj.attributes.target_temp_low !== undefined &&
stateObj.attributes.target_temp_low !== null) ||
(stateObj.attributes.target_temp_high !== undefined &&
stateObj.attributes.target_temp_high !== null)
? html`
<ha-climate-control
.value=${stateObj.attributes.target_temp_low}

View File

@@ -16,6 +16,7 @@ import "./more-info-input_datetime";
import "./more-info-light";
import "./more-info-lock";
import "./more-info-media_player";
import "./more-info-person";
import "./more-info-script";
import "./more-info-sun";
import "./more-info-timer";

View File

@@ -0,0 +1,86 @@
import {
LitElement,
html,
TemplateResult,
CSSResult,
css,
property,
customElement,
} from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import "@material/mwc-button";
import "../../../components/map/ha-map";
import { HomeAssistant } from "../../../types";
import { showZoneEditor } from "../../../data/zone";
import { fireEvent } from "../../../common/dom/fire_event";
@customElement("more-info-person")
class MoreInfoPerson extends LitElement {
@property() public hass!: HomeAssistant;
@property() public stateObj?: HassEntity;
protected render(): TemplateResult {
if (!this.hass || !this.stateObj) {
return html``;
}
return html`
<ha-attributes
.stateObj=${this.stateObj}
extraFilters="id,user_id,editable"
></ha-attributes>
${this.stateObj.attributes.latitude && this.stateObj.attributes.longitude
? html`
<ha-map
.hass=${this.hass}
.entities=${[this.stateObj.entity_id]}
></ha-map>
`
: ""}
${!__DEMO__ &&
this.hass.user?.is_admin &&
this.stateObj.state === "not_home" &&
this.stateObj.attributes.latitude &&
this.stateObj.attributes.longitude
? html`
<div class="actions">
<mwc-button @click=${this._handleAction}>
${this.hass.localize(
"ui.dialogs.more_info_control.person.create_zone"
)}
</mwc-button>
</div>
`
: ""}
`;
}
private _handleAction() {
showZoneEditor(this, {
latitude: this.stateObj!.attributes.latitude,
longitude: this.stateObj!.attributes.longitude,
});
fireEvent(this, "hass-more-info", { entityId: null });
}
static get styles(): CSSResult {
return css`
.flex {
display: flex;
justify-content: space-between;
}
.actions {
margin: 36px 0 8px 0;
text-align: right;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-person": MoreInfoPerson;
}
}

View File

@@ -11,7 +11,7 @@ import { HassEntity } from "home-assistant-js-websocket";
import "../../../components/ha-relative-time";
import formatTime from "../../../common/datetime/format_time";
import { formatTime } from "../../../common/datetime/format_time";
import { HomeAssistant } from "../../../types";
@customElement("more-info-sun")

View File

@@ -1,263 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-attributes";
import "../../../components/ha-paper-dropdown-menu";
import { supportsFeature } from "../../../common/entity/supports-feature";
class MoreInfoVacuum extends PolymerElement {
static get template() {
return html`
<style include="iron-flex iron-flex-alignment"></style>
<style>
:host {
@apply --paper-font-body1;
line-height: 1.5;
}
.status-subtitle {
color: var(--secondary-text-color);
}
paper-item {
cursor: pointer;
}
</style>
<div class="horizontal justified layout">
<div hidden$="[[!supportsStatus(stateObj)]]">
<span class="status-subtitle">Status: </span
><span><strong>[[stateObj.attributes.status]]</strong></span>
</div>
<div hidden$="[[!supportsBattery(stateObj)]]">
<span
><iron-icon icon="[[stateObj.attributes.battery_icon]]"></iron-icon>
[[stateObj.attributes.battery_level]] %</span
>
</div>
</div>
<div hidden$="[[!supportsCommandBar(stateObj)]]">
<p></p>
<div class="status-subtitle">Vacuum cleaner commands:</div>
<div class="horizontal justified layout">
<template is="dom-if" if="[[supportsStart(stateObj)]]">
<div>
<paper-icon-button
icon="hass:play"
on-click="onStart"
title="Start"
></paper-icon-button>
</div>
<div hidden$="[[!supportsPause(stateObj)]]">
<paper-icon-button
icon="hass:pause"
on-click="onPause"
title="Pause"
></paper-icon-button>
</div>
</template>
<template is="dom-if" if="[[!supportsStart(stateObj)]]">
<div hidden$="[[!supportsPause(stateObj)]]">
<paper-icon-button
icon="hass:play-pause"
on-click="onPlayPause"
title="Pause"
></paper-icon-button>
</div>
</template>
<div hidden$="[[!supportsStop(stateObj)]]">
<paper-icon-button
icon="hass:stop"
on-click="onStop"
title="Stop"
></paper-icon-button>
</div>
<div hidden$="[[!supportsCleanSpot(stateObj)]]">
<paper-icon-button
icon="hass:broom"
on-click="onCleanSpot"
title="Clean spot"
></paper-icon-button>
</div>
<div hidden$="[[!supportsLocate(stateObj)]]">
<paper-icon-button
icon="hass:map-marker"
on-click="onLocate"
title="Locate"
></paper-icon-button>
</div>
<div hidden$="[[!supportsReturnHome(stateObj)]]">
<paper-icon-button
icon="hass:home-map-marker"
on-click="onReturnHome"
title="Return home"
></paper-icon-button>
</div>
</div>
</div>
<div hidden$="[[!supportsFanSpeed(stateObj)]]">
<div class="horizontal justified layout">
<ha-paper-dropdown-menu
label-float=""
dynamic-align=""
label="Fan speed"
>
<paper-listbox
slot="dropdown-content"
selected="[[stateObj.attributes.fan_speed]]"
on-selected-changed="fanSpeedChanged"
attr-for-selected="item-name"
>
<template
is="dom-repeat"
items="[[stateObj.attributes.fan_speed_list]]"
>
<paper-item item-name$="[[item]]">[[item]]</paper-item>
</template>
</paper-listbox>
</ha-paper-dropdown-menu>
<div
style="justify-content: center; align-self: center; padding-top: 1.3em"
>
<span
><iron-icon icon="hass:fan"></iron-icon>
[[stateObj.attributes.fan_speed]]</span
>
</div>
</div>
<p></p>
</div>
<ha-attributes
state-obj="[[stateObj]]"
extra-filters="fan_speed,fan_speed_list,status,battery_level,battery_icon"
></ha-attributes>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
inDialog: {
type: Boolean,
value: false,
},
stateObj: {
type: Object,
},
};
}
supportsPause(stateObj) {
return supportsFeature(stateObj, 4);
}
supportsStop(stateObj) {
return supportsFeature(stateObj, 8);
}
supportsReturnHome(stateObj) {
return supportsFeature(stateObj, 16);
}
supportsFanSpeed(stateObj) {
return supportsFeature(stateObj, 32);
}
supportsBattery(stateObj) {
return supportsFeature(stateObj, 64);
}
supportsStatus(stateObj) {
return supportsFeature(stateObj, 128);
}
supportsLocate(stateObj) {
return supportsFeature(stateObj, 512);
}
supportsCleanSpot(stateObj) {
return supportsFeature(stateObj, 1024);
}
supportsStart(stateObj) {
return supportsFeature(stateObj, 8192);
}
supportsCommandBar(stateObj) {
return (
supportsFeature(stateObj, 4) |
supportsFeature(stateObj, 8) |
supportsFeature(stateObj, 16) |
supportsFeature(stateObj, 512) |
supportsFeature(stateObj, 1024) |
supportsFeature(stateObj, 8192)
);
}
fanSpeedChanged(ev) {
var oldVal = this.stateObj.attributes.fan_speed;
var newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;
this.hass.callService("vacuum", "set_fan_speed", {
entity_id: this.stateObj.entity_id,
fan_speed: newVal,
});
}
onStop() {
this.hass.callService("vacuum", "stop", {
entity_id: this.stateObj.entity_id,
});
}
onPlayPause() {
this.hass.callService("vacuum", "start_pause", {
entity_id: this.stateObj.entity_id,
});
}
onPause() {
this.hass.callService("vacuum", "pause", {
entity_id: this.stateObj.entity_id,
});
}
onStart() {
this.hass.callService("vacuum", "start", {
entity_id: this.stateObj.entity_id,
});
}
onLocate() {
this.hass.callService("vacuum", "locate", {
entity_id: this.stateObj.entity_id,
});
}
onCleanSpot() {
this.hass.callService("vacuum", "clean_spot", {
entity_id: this.stateObj.entity_id,
});
}
onReturnHome() {
this.hass.callService("vacuum", "return_to_base", {
entity_id: this.stateObj.entity_id,
});
}
}
customElements.define("more-info-vacuum", MoreInfoVacuum);

View File

@@ -0,0 +1,259 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { HomeAssistant } from "../../../types";
import "../../../components/ha-paper-dropdown-menu";
import "../../../components/ha-attributes";
import {
VACUUM_SUPPORT_BATTERY,
VACUUM_SUPPORT_CLEAN_SPOT,
VACUUM_SUPPORT_FAN_SPEED,
VACUUM_SUPPORT_LOCATE,
VACUUM_SUPPORT_PAUSE,
VACUUM_SUPPORT_RETURN_HOME,
VACUUM_SUPPORT_START,
VACUUM_SUPPORT_STATUS,
VACUUM_SUPPORT_STOP,
VacuumEntity,
} from "../../../data/vacuum";
interface VacuumCommand {
translationKey: string;
icon: string;
serviceName: string;
isVisible: (stateObj: VacuumEntity) => boolean;
}
const VACUUM_COMMANDS: VacuumCommand[] = [
{
translationKey: "start",
icon: "hass:play",
serviceName: "start",
isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_START),
},
{
translationKey: "pause",
icon: "hass:pause",
serviceName: "pause",
isVisible: (stateObj) =>
// We need also to check if Start is supported because if not we show play-pause
supportsFeature(stateObj, VACUUM_SUPPORT_START) &&
supportsFeature(stateObj, VACUUM_SUPPORT_PAUSE),
},
{
translationKey: "start_pause",
icon: "hass:play-pause",
serviceName: "start_pause",
isVisible: (stateObj) =>
// If start is supported, we don't show this button
!supportsFeature(stateObj, VACUUM_SUPPORT_START) &&
supportsFeature(stateObj, VACUUM_SUPPORT_PAUSE),
},
{
translationKey: "stop",
icon: "hass:stop",
serviceName: "stop",
isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_STOP),
},
{
translationKey: "clean_spot",
icon: "hass:broom",
serviceName: "clean_spot",
isVisible: (stateObj) =>
supportsFeature(stateObj, VACUUM_SUPPORT_CLEAN_SPOT),
},
{
translationKey: "locate",
icon: "hass:map-marker",
serviceName: "locate",
isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_LOCATE),
},
{
translationKey: "return_home",
icon: "hass:home-map-marker",
serviceName: "return_to_base",
isVisible: (stateObj) =>
supportsFeature(stateObj, VACUUM_SUPPORT_RETURN_HOME),
},
];
@customElement("more-info-vacuum")
class MoreInfoVacuum extends LitElement {
@property() public hass!: HomeAssistant;
@property() public stateObj?: VacuumEntity;
protected render(): TemplateResult {
if (!this.hass || !this.stateObj) {
return html``;
}
const stateObj = this.stateObj;
const filterExtraAttributes =
"fan_speed,fan_speed_list,status,battery_level,battery_icon";
return html`
<div class="flex-horizontal">
${supportsFeature(stateObj, VACUUM_SUPPORT_STATUS)
? html`
<div>
<span class="status-subtitle"
>${this.hass!.localize(
"ui.dialogs.more_info_control.vacuum.status"
)}:
</span>
<span><strong>${stateObj.attributes.status}</strong></span>
</div>
`
: ""}
${supportsFeature(stateObj, VACUUM_SUPPORT_BATTERY)
? html`
<div>
<span>
<iron-icon
.icon=${stateObj.attributes.battery_icon}
></iron-icon>
${stateObj.attributes.battery_level}%
</span>
</div>
`
: ""}
</div>
${VACUUM_COMMANDS.some((item) => item.isVisible(stateObj))
? html`
<div>
<p></p>
<div class="status-subtitle">
${this.hass!.localize(
"ui.dialogs.more_info_control.vacuum.commands"
)}
</div>
<div class="flex-horizontal">
${VACUUM_COMMANDS.filter((item) =>
item.isVisible(stateObj)
).map(
(item) => html`
<div>
<paper-icon-button
.icon=${item.icon}
.entry=${item}
@click=${this.callService}
.title=${this.hass!.localize(
`ui.dialogs.more_info_control.vacuum.${item.translationKey}`
)}
></paper-icon-button>
</div>
`
)}
</div>
</div>
`
: ""}
${supportsFeature(stateObj, VACUUM_SUPPORT_FAN_SPEED)
? html`
<div>
<div class="flex-horizontal">
<ha-paper-dropdown-menu
.label=${this.hass!.localize(
"ui.dialogs.more_info_control.vacuum.fan_speed"
)}
>
<paper-listbox
slot="dropdown-content"
.selected=${stateObj.attributes.fan_speed}
@iron-select=${this.handleFanSpeedChanged}
attr-for-selected="item-name"
>
${stateObj.attributes.fan_speed_list!.map(
(mode) => html`
<paper-item .itemName=${mode}>
${mode}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
<div
style="justify-content: center; align-self: center; padding-top: 1.3em"
>
<span>
<iron-icon icon="hass:fan"></iron-icon>
${stateObj.attributes.fan_speed}
</span>
</div>
</div>
<p></p>
</div>
`
: ""}
<ha-attributes
.stateObj=${this.stateObj}
.extraFilters=${filterExtraAttributes}
></ha-attributes>
`;
}
private callService(ev: CustomEvent) {
const entry = (ev.target! as any).entry as VacuumCommand;
this.hass.callService("vacuum", entry.serviceName, {
entity_id: this.stateObj!.entity_id,
});
}
private handleFanSpeedChanged(ev: CustomEvent) {
const oldVal = this.stateObj!.attributes.fan_speed;
const newVal = ev.detail.item.itemName;
if (!newVal || oldVal === newVal) {
return;
}
this.hass.callService("vacuum", "set_fan_speed", {
entity_id: this.stateObj!.entity_id,
fan_speed: newVal,
});
}
static get styles(): CSSResult {
return css`
:host {
@apply --paper-font-body1;
line-height: 1.5;
}
.status-subtitle {
color: var(--secondary-text-color);
}
paper-item {
cursor: pointer;
}
.flex-horizontal {
display: flex;
flex-direction: row;
justify-content: space-between;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-vacuum": MoreInfoVacuum;
}
}

View File

@@ -22,7 +22,7 @@ import LocalizeMixin from "../../mixins/localize-mixin";
import { computeRTL } from "../../common/util/compute_rtl";
import { removeEntityRegistryEntry } from "../../data/entity_registry";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { showEntityRegistryDetailDialog } from "../../panels/config/entities/show-dialog-entity-registry-detail";
import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor";
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
@@ -88,7 +88,7 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class="main-title" main-title="" on-click="enlarge">
[[_computeStateName(stateObj)]]
</div>
<template is="dom-if" if="[[registryEntry]]">
<template is="dom-if" if="[[_computeConfig(hass)]]">
<paper-icon-button
aria-label$="[[localize('ui.dialogs.more_info_control.settings')]]"
icon="hass:settings"
@@ -221,6 +221,10 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
return stateObj ? computeStateName(stateObj) : "";
}
_computeConfig(hass) {
return hass.user.is_admin && isComponentLoaded(hass, "config");
}
_computeEdit(hass, stateObj) {
const domain = this._computeDomain(stateObj);
return (
@@ -260,7 +264,9 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
_gotoSettings() {
showEntityRegistryDetailDialog(this, { entry: this.registryEntry });
showEntityEditorDialog(this, {
entity_id: this.stateObj.entity_id,
});
this.fire("hass-more-info", { entityId: null });
}

View File

@@ -36,13 +36,13 @@ export class HuiNotificationItem extends LitElement {
return "entity_id" in this.notification
? html`
<configurator-notification-item
.hass="${this.hass}"
.hass=${this.hass}
.notification="${this.notification}"
></configurator-notification-item>
`
: html`
<persistent-notification-item
.hass="${this.hass}"
.hass=${this.hass}
.notification="${this.notification}"
></persistent-notification-item>
`;

View File

@@ -39,7 +39,7 @@ export class HuiPersistentNotificationItem extends LitElement {
<div class="time">
<span>
<ha-relative-time
.hass="${this.hass}"
.hass=${this.hass}
.datetime="${this.notification.created_at}"
></ha-relative-time>
<paper-tooltip

View File

@@ -10,12 +10,18 @@ import {
} from "home-assistant-js-websocket";
import { loadTokens, saveTokens } from "../common/auth/token_storage";
import { isExternal } from "../data/external";
import { subscribePanels } from "../data/ws-panels";
import { subscribeThemes } from "../data/ws-themes";
import { subscribeUser } from "../data/ws-user";
import { HomeAssistant } from "../types";
import { hassUrl } from "../data/auth";
import { fetchConfig, WindowWithLovelaceProm } from "../data/lovelace";
import { subscribeFrontendUserData } from "../data/frontend";
import {
fetchConfig,
fetchResources,
WindowWithLovelaceProm,
} from "../data/lovelace";
declare global {
interface Window {
@@ -23,11 +29,6 @@ declare global {
}
}
const isExternal =
window.externalApp ||
window.webkit?.messageHandlers?.getExternalAuth ||
location.search.includes("external_auth=1");
const authProm = isExternal
? () =>
import(
@@ -88,9 +89,15 @@ window.hassConnection.then(({ conn }) => {
subscribePanels(conn, noop);
subscribeThemes(conn, noop);
subscribeUser(conn, noop);
subscribeFrontendUserData(conn, "core", noop);
if (location.pathname === "/" || location.pathname.startsWith("/lovelace/")) {
(window as WindowWithLovelaceProm).llConfProm = fetchConfig(conn, false);
(window as WindowWithLovelaceProm).llConfProm = fetchConfig(
conn,
null,
false
);
(window as WindowWithLovelaceProm).llResProm = fetchResources(conn);
}
});

View File

@@ -17,4 +17,5 @@ export const demoConfig: HassConfig = {
version: "DEMO",
whitelist_external_dirs: [],
config_source: "storage",
safe_mode: false,
};

View File

@@ -0,0 +1,157 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import "../components/data-table/ha-data-table";
// tslint:disable-next-line
import {
HaDataTable,
DataTableColumnContainer,
DataTableRowData,
} from "../components/data-table/ha-data-table";
import "./hass-tabs-subpage";
import { HomeAssistant, Route } from "../types";
// tslint:disable-next-line
import { PageNavigation } from "./hass-tabs-subpage";
@customElement("hass-tabs-subpage-data-table")
export class HaTabsSubpageDataTable extends LitElement {
@property() public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
/**
* Object with the columns.
* @type {Object}
*/
@property({ type: Object }) public columns: DataTableColumnContainer = {};
/**
* Data to show in the table.
* @type {Array}
*/
@property({ type: Array }) public data: DataTableRowData[] = [];
/**
* Should rows be selectable.
* @type {Boolean}
*/
@property({ type: Boolean }) public selectable = false;
/**
* Field with a unique id per entry in data.
* @type {String}
*/
@property({ type: String }) public id = "id";
/**
* String to filter the data in the data table on.
* @type {String}
*/
@property({ type: String }) public filter = "";
/**
* What path to use when the back button is pressed.
* @type {String}
* @attr back-path
*/
@property({ type: String, attribute: "back-path" }) public backPath?: string;
/**
* Function to call when the back button is pressed.
* @type {() => void}
*/
@property() public backCallback?: () => void;
@property() public route!: Route;
/**
* Array of tabs to show on the page.
* @type {Array}
*/
@property() public tabs!: PageNavigation[];
@query("ha-data-table") private _dataTable!: HaDataTable;
public clearSelection() {
this._dataTable.clearSelection();
}
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this.backPath}
.backCallback=${this.backCallback}
.route=${this.route}
.tabs=${this.tabs}
>
${this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="search-toolbar">
<search-input
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
></search-input>
</div>
</slot>
</div>
`
: ""}
<ha-data-table
.columns=${this.columns}
.data=${this.data}
.filter=${this.filter}
.selectable=${this.selectable}
.id=${this.id}
>
${!this.narrow
? html`
<div slot="header">
<slot name="header">
<slot name="header">
<div class="table-header">
<search-input
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
></search-input></div></slot
></slot>
</div>
`
: html`
<div slot="header"></div>
`}
</ha-data-table>
</hass-tabs-subpage>
`;
}
private _handleSearchChange(ev: CustomEvent) {
this.filter = ev.detail.value;
}
static get styles(): CSSResult {
return css`
ha-data-table {
width: 100%;
--data-table-border-width: 0;
}
:host(:not([narrow])) ha-data-table {
height: calc(100vh - 65px);
display: block;
}
.table-header {
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
.search-toolbar {
margin-left: -24px;
color: var(--secondary-text-color);
}
search-input {
position: relative;
top: 2px;
}
`;
}
}

View File

@@ -15,6 +15,7 @@ import { Route, HomeAssistant } from "../types";
import { navigate } from "../common/navigate";
import "@material/mwc-ripple";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import memoizeOne from "memoize-one";
export interface PageNavigation {
path: string;
@@ -22,7 +23,7 @@ export interface PageNavigation {
component?: string;
name?: string;
core?: boolean;
exportOnly?: boolean;
advancedOnly?: boolean;
icon?: string;
info?: any;
}
@@ -33,12 +34,57 @@ class HassTabsSubpage extends LitElement {
@property({ type: String, attribute: "back-path" }) public backPath?: string;
@property() public backCallback?: () => void;
@property({ type: Boolean }) public hassio = false;
@property({ type: Boolean }) public showAdvanced = false;
@property() public route!: Route;
@property() public tabs!: PageNavigation[];
@property({ type: Boolean, reflect: true }) public narrow = false;
@property() private _activeTab: number = -1;
private _getTabs = memoizeOne(
(
tabs: PageNavigation[],
activeTab: number,
showAdvanced: boolean | undefined,
_components,
_language
) => {
const shownTabs = tabs.filter(
(page) =>
(!page.component ||
page.core ||
isComponentLoaded(this.hass, page.component)) &&
(!page.advancedOnly || showAdvanced)
);
return shownTabs.map(
(page, index) => html`
<div
class="tab ${classMap({
active: index === activeTab,
})}"
@click=${this._tabTapped}
.path=${page.path}
>
${this.narrow
? html`
<ha-icon .icon=${page.icon}></ha-icon>
`
: ""}
${!this.narrow || index === activeTab
? html`
<span class="name"
>${page.translationKey
? this.hass.localize(page.translationKey)
: name}</span
>
`
: ""}
<mwc-ripple></mwc-ripple>
</div>
`
);
}
);
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("route")) {
@@ -49,6 +95,14 @@ class HassTabsSubpage extends LitElement {
}
protected render(): TemplateResult {
const tabs = this._getTabs(
this.tabs,
this._activeTab,
this.hass.userData?.showAdvanced,
this.hass.config.components,
this.hass.language
);
return html`
<div class="toolbar">
<ha-paper-icon-button-arrow-prev
@@ -56,41 +110,18 @@ class HassTabsSubpage extends LitElement {
.hassio=${this.hassio}
@click=${this._backTapped}
></ha-paper-icon-button-arrow-prev>
<div id="tabbar" class=${classMap({ "bottom-bar": this.narrow })}>
${this.tabs.map((page, index) =>
(!page.component ||
page.core ||
isComponentLoaded(this.hass, page.component)) &&
(!page.exportOnly || this.showAdvanced)
? html`
<div
class="tab ${classMap({
active: index === this._activeTab,
})}"
@click=${this._tabTapped}
.path=${page.path}
>
${this.narrow
? html`
<ha-icon .icon=${page.icon}></ha-icon>
`
: ""}
${!this.narrow || index === this._activeTab
? html`
<span class="name"
>${page.translationKey
? this.hass.localize(page.translationKey)
: name}</span
>
`
: ""}
<mwc-ripple></mwc-ripple>
</div>
`
: ""
)}
</div>
${this.narrow
? html`
<div main-title><slot name="header"></slot></div>
`
: ""}
${tabs.length > 1 || !this.narrow
? html`
<div id="tabbar" class=${classMap({ "bottom-bar": this.narrow })}>
${tabs}
</div>
`
: ""}
<div id="toolbar-icon">
<slot name="toolbar-icon"></slot>
</div>
@@ -138,11 +169,6 @@ class HassTabsSubpage extends LitElement {
box-sizing: border-box;
}
:host([narrow]) .toolbar {
background-color: var(--primary-background-color);
border-bottom: none;
}
#tabbar {
display: flex;
font-size: 14px;

View File

@@ -9,7 +9,7 @@ import {
} from "./hass-router-page";
import { removeInitSkeleton } from "../util/init-skeleton";
const CACHE_COMPONENTS = ["lovelace", "states", "developer-tools"];
const CACHE_URL_PATHS = ["lovelace", "states", "developer-tools"];
const COMPONENTS = {
calendar: () =>
import(
@@ -69,11 +69,10 @@ const COMPONENTS = {
const getRoutes = (panels: Panels): RouterOptions => {
const routes: RouterOptions["routes"] = {};
Object.values(panels).forEach((panel) => {
const data: RouteOptions = {
tag: `ha-panel-${panel.component_name}`,
cache: CACHE_COMPONENTS.includes(panel.component_name),
cache: CACHE_URL_PATHS.includes(panel.url_path),
};
if (panel.component_name in COMPONENTS) {
data.load = COMPONENTS[panel.component_name];

View File

@@ -82,6 +82,10 @@ class NotificationManager extends LitElement {
static get styles(): CSSResult {
return css`
:host {
display: flex;
align-items: center;
}
mwc-button {
color: var(--primary-color);
font-weight: bold;

View File

@@ -6,6 +6,7 @@ import {
TemplateResult,
property,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";

View File

@@ -13,6 +13,8 @@ import { Action } from "../../../../data/script";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-action-row";
import { HaDeviceAction } from "./types/ha-automation-action-device_id";
@customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement {
@property() public hass!: HomeAssistant;
@@ -46,7 +48,7 @@ export default class HaAutomationAction extends LitElement {
private _addAction() {
const actions = this.actions.concat({
service: "",
...HaDeviceAction.defaultConfig,
});
fireEvent(this, "value-changed", { value: actions });

View File

@@ -9,7 +9,7 @@ import {
import "@material/mwc-button";
import "../../../../components/ha-card";
import { HaStateCondition } from "./types/ha-automation-condition-state";
import { HaDeviceCondition } from "./types/ha-automation-condition-device";
import { fireEvent } from "../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../types";
@@ -48,8 +48,8 @@ export default class HaAutomationCondition extends LitElement {
private _addCondition() {
const conditions = this.conditions.concat({
condition: "state",
...HaStateCondition.defaultConfig,
condition: "device",
...HaDeviceCondition.defaultConfig,
});
fireEvent(this, "value-changed", { value: conditions });

View File

@@ -26,7 +26,7 @@ export default class HaNumericStateCondition extends LitElement {
<ha-entity-picker
.value="${entity_id}"
@value-changed="${this._entityPicked}"
.hass="${this.hass}"
.hass=${this.hass}
allow-custom-entity
></ha-entity-picker>
<paper-input

View File

@@ -22,6 +22,7 @@ import {
deleteAutomation,
getAutomationEditorInitData,
Trigger,
triggerAutomation,
} from "../../../data/automation";
import { Action } from "../../../data/script";
import {
@@ -36,6 +37,8 @@ import "./condition/ha-automation-condition";
import "./trigger/ha-automation-trigger";
import "../../../layouts/hass-tabs-subpage";
import { configSections } from "../ha-panel-config";
import { HaDeviceAction } from "./action/types/ha-automation-action-device_id";
import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device";
export class HaAutomationEditor extends LitElement {
@property() public hass!: HomeAssistant;
@@ -74,132 +77,155 @@ export class HaAutomationEditor extends LitElement {
<div class="errors">${this._errors}</div>
`
: ""}
<div
class="${classMap({
rtl: computeRTL(this.hass),
})}"
>
${this._config
? html`
<ha-config-section .isWide=${this.isWide}>
<span slot="header">${this._config.alias}</span>
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
name="alias"
.value=${this._config.alias}
@value-changed=${this._valueChanged}
>
</paper-input>
<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"
.value=${this._config.description}
@value-changed=${this._valueChanged}
></ha-textarea>
</div>
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.introduction"
${this._config
? html`
${this.narrow
? html`
<span slot="header">${this._config?.alias}</span>
`
: ""}
<ha-config-section .isWide=${this.isWide}>
${!this.narrow
? html`
<span slot="header">${this._config.alias}</span>
`
: ""}
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
</p>
<a
href="https://home-assistant.io/docs/automation/trigger/"
target="_blank"
name="alias"
.value=${this._config.alias}
@value-changed=${this._valueChanged}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.learn_more"
</paper-input>
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
</a>
</span>
<ha-automation-trigger
.triggers=${this._config.trigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger>
</ha-config-section>
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
.value=${this._config.description}
@value-changed=${this._valueChanged}
></ha-textarea>
</div>
${this.creatingNew
? ""
: html`
<div
class="card-actions layout horizontal justified center"
>
<div class="layout horizontal center">
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${this.automation}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
<mwc-button @click=${this._excuteAutomation}>
${this.hass.localize("ui.card.automation.trigger")}
</mwc-button>
</div>
`}
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header"
"ui.panel.config.automation.editor.triggers.introduction"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/scripts/conditions/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.learn_more"
)}
</a>
</span>
<ha-automation-condition
.conditions=${this._config.condition || []}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition>
</ha-config-section>
</p>
<a
href="https://home-assistant.io/docs/automation/trigger/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.learn_more"
)}
</a>
</span>
<ha-automation-trigger
.triggers=${this._config.trigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.header"
"ui.panel.config.automation.editor.conditions.introduction"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/automation/action/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.learn_more"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.action}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>
`
: ""}
</div>
</p>
<a
href="https://home-assistant.io/docs/scripts/conditions/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.learn_more"
)}
</a>
</span>
<ha-automation-condition
.conditions=${this._config.condition || []}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/automation/action/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.learn_more"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.action}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>
`
: ""}
<ha-fab
?is-wide="${this.isWide}"
?narrow="${this.narrow}"
@@ -273,9 +299,9 @@ export class HaAutomationEditor extends LitElement {
"ui.panel.config.automation.editor.default_name"
),
description: "",
trigger: [{ platform: "state" }],
trigger: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
condition: [],
action: [{ service: "" }],
action: [{ ...HaDeviceAction.defaultConfig }],
...initData,
};
}
@@ -317,6 +343,10 @@ export class HaAutomationEditor extends LitElement {
this._dirty = true;
}
private _excuteAutomation() {
triggerAutomation(this.hass, this.automation.entity_id);
}
private _backTapped(): void {
if (this._dirty) {
showConfirmationDialog(this, {
@@ -389,6 +419,9 @@ export class HaAutomationEditor extends LitElement {
span[slot="introduction"] a {
color: var(--primary-color);
}
ha-entity-toggle {
margin-right: 8px;
}
ha-fab {
position: fixed;
bottom: 16px;

View File

@@ -28,7 +28,7 @@ import {
showAutomationEditor,
AutomationConfig,
} from "../../../data/automation";
import format_date_time from "../../../common/datetime/format_date_time";
import { formatDateTime } from "../../../common/datetime/format_date_time";
import { fireEvent } from "../../../common/dom/fire_event";
import { showThingtalkDialog } from "./show-dialog-thingtalk";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@@ -102,7 +102,7 @@ class HaAutomationPicker extends LitElement {
"ui.card.automation.last_triggered"
)}: ${
automation.attributes.last_triggered
? format_date_time(
? formatDateTime(
new Date(automation.attributes.last_triggered),
this.hass.language
)

View File

@@ -13,7 +13,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-trigger-row";
import { HaStateTrigger } from "./types/ha-automation-trigger-state";
import { HaDeviceTrigger } from "./types/ha-automation-trigger-device";
import { Trigger } from "../../../../data/automation";
@customElement("ha-automation-trigger")
@@ -47,8 +47,8 @@ export default class HaAutomationTrigger extends LitElement {
private _addTrigger() {
const triggers = this.triggers.concat({
platform: "state",
...HaStateTrigger.defaultConfig,
platform: "device",
...HaDeviceTrigger.defaultConfig,
});
fireEvent(this, "value-changed", { value: triggers });

View File

@@ -42,7 +42,7 @@ export default class HaNumericStateTrigger extends LitElement {
<ha-entity-picker
.value="${entity_id}"
@value-changed="${this._entityPicked}"
.hass="${this.hass}"
.hass=${this.hass}
allow-custom-entity
></ha-entity-picker>
<paper-input

View File

@@ -16,7 +16,7 @@ import "./cloud-remote-pref";
import { EventsMixin } from "../../../../mixins/events-mixin";
import { fetchCloudSubscriptionInfo } from "../../../../data/cloud";
import formatDateTime from "../../../../common/datetime/format_date_time";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import LocalizeMixin from "../../../../mixins/localize-mixin";
/*

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