Compare commits

...

131 Commits

Author SHA1 Message Date
Bram Kragten
c1eabeb29f Remove light card for generated mode 2020-01-17 21:53:38 +01:00
Paulus Schoutsen
5ff8fe68ba Alert user when add-on not started (#4485) 2020-01-17 17:02:56 +01:00
David F. Mulcahey
a2a039ebc5 Add group binding to the ZHA config panel and misc. cleanup (#4466)
* clean up zha device card and usage

* group binding tile

* add cluster selection to group binding tile

* fix css class name

* fix filtering

* multiselect for clusters in group binding

* pass narrow to cluster table

* fix tables

* fix device page

* address remaing comments from previous PR

* fix bad cherry-pick

* css cleanup

* consistency

* use properties

* translations

* add confirmation dialog to remove button

* fix css

* review comments

* remove noise
2020-01-17 16:39:57 +01:00
Ian Richardson
1064aed1b0 📝 make some Lovelace UI text more clear (#4500) 2020-01-17 09:54:16 +01:00
Ian Richardson
7025592e8e 🐛 fix picture glance card's camera_view option in editor (#4495) 2020-01-17 09:44:57 +01:00
HomeAssistant Azure
4966354b62 [ci skip] Translation update 2020-01-17 00:32:37 +00:00
David F. Mulcahey
68d6faf4af fix selection check (#4488) 2020-01-16 18:19:01 +01:00
Paulus Schoutsen
e3346483b9 Hide device trackers from generated lovelace (#4487) 2020-01-16 08:57:41 +01:00
HomeAssistant Azure
e8fb79e5ce [ci skip] Translation update 2020-01-16 00:32:40 +00:00
Alexei Chetroi
d612162ab1 Fix ZHA add device path. (#4486) 2020-01-15 20:05:05 +01:00
Bram Kragten
86f8ef3a70 Styling focus menus (#4483)
* Styling menus

* Update ha-config-navigation.ts
2020-01-15 19:41:56 +01:00
Bram Kragten
0e43435362 Don't ask to choose view when only 1 view (#4480) 2020-01-15 09:05:01 -08:00
Bram Kragten
aaefe0b09f Handle unknown state (#4481) 2020-01-15 09:01:59 -08:00
Bram Kragten
bc731a9dc3 Add edit btn to more info for scene, script and automation (#4476) 2020-01-15 09:50:16 +01:00
Bram Kragten
da25701dca Disable adoptedStyleSheets in dev (#4474) 2020-01-15 09:25:17 +01:00
Bram Kragten
21ae483dc9 Styling fixes (#4475) 2020-01-15 09:25:04 +01:00
HomeAssistant Azure
38b6e9ca10 [ci skip] Translation update 2020-01-15 00:32:57 +00:00
Bram Kragten
d31245866c Add DEPRECATED to states ui (#4463)
* Add DEPRECATED to states ui

* unelevated red

* target

* Add msg in info
2020-01-14 06:35:01 -08:00
Bram Kragten
4e08d8f3b3 Fix zha back btn (#4470) 2020-01-14 07:57:00 -05:00
Bram Kragten
1e717ab33e Catch undefined cloudstatus (#4465) 2020-01-14 13:52:23 +01:00
Bram Kragten
995fb4974e Fix translations (#4469) 2020-01-14 13:20:06 +01:00
HomeAssistant Azure
ffb76132f8 [ci skip] Translation update 2020-01-14 00:32:29 +00:00
Bram Kragten
acba3af54b Fix back btn for Polymer (#4467) 2020-01-13 18:21:43 +01:00
Paulus Schoutsen
40ac456937 Force refresh tokens if external app (#4461) 2020-01-13 05:47:08 -08:00
Bram Kragten
5c32413bf7 Onboarding core: Display error message when saving fails (#4462) 2020-01-13 05:31:53 -08:00
Bram Kragten
22792c70c5 Change config panel navigation (#4377)
* Change config panel navigation

* Show active + don't show toolbar?

* Update ha-panel-config.ts

* Change color of menu toolbar

* Update ha-config-router.ts

* Review comments
2020-01-12 17:57:38 +01:00
Krisjanis Lejejs
a8ed87298a Improved map panel and map card to ignore zones when fitting map. (#4447)
* Improved map panel and map card to ignore zones when fitting map. [#1598](https://github.com/home-assistant/home-assistant-polymer/issues/1598)

* Improved map panel and map card to ignore zones when fitting map. [#1598](https://github.com/home-assistant/home-assistant-polymer/issues/1598)

* Improved map panel and map card to ignore zones when fitting map. [#1598](https://github.com/home-assistant/home-assistant-polymer/issues/1598)

* Changed approach and created a different array for zones

* Removed zone key option for markers
2020-01-12 17:56:55 +01:00
Joakim Sørensen
b15270dfe2 Use correct suffix for elevation (#4454)
* Use correct suffix for elevation

* Use correct suffix for elevation
2020-01-12 07:31:59 -08:00
Bram Kragten
58ad949bc8 Virtualize logbook (#4450)
* Virtualize logbook

* Clean

* Update ha-logbook.ts
2020-01-12 13:00:26 +01:00
HomeAssistant Azure
adce40de56 [ci skip] Translation update 2020-01-12 00:33:31 +00:00
Ian Richardson
0f487ae4bf Add tabindex to lovelace elements (#4160)
* tabindex

* use action handler

* circular focus test

* address comment

* add focus styling to other elements

* add focus styling to cards

* style glance card entities

* Add back light/thermo changes that were lost in rebase

* Remove unused import

* lint

* lint

* 💄 tweak focus style for glance entities

* 💄 apply styling to focused state-label-badges
2020-01-11 11:50:43 +01:00
Joakim Sørensen
2848e3a63b Adds CCS var usage to person dialog (#4449) 2020-01-11 11:49:57 +01:00
Bram Kragten
5a172a64c5 Make entry flow dialog modal (#4440)
* Make entry flow dialog modal

* Add close button

* Update dialog-data-entry-flow.ts

* Fix aria-label
2020-01-10 16:40:19 -08:00
HomeAssistant Azure
433aa16ea6 [ci skip] Translation update 2020-01-11 00:32:34 +00:00
HomeAssistant Azure
50cb8cf3cc [ci skip] Translation update 2020-01-10 00:32:38 +00:00
Sean Mooney
4e5406b27b Typo fix in issue template (#4445)
fixes small typo, necesarry = necessary
2020-01-09 09:29:42 -06:00
Franck Nijhof
80eb80619a Add configuration for Lock Threads on closed pull requests (#4443) 2020-01-09 11:40:25 +01:00
Ian Richardson
bf71b3a869 ♻️ convert ha-attributes to lit-element (#4350)
* ♻️ convert ha-attributes to lit-element

* Address comments

* inline items

* 🐛 Fix attribution display logic
2020-01-09 10:22:23 +01:00
HomeAssistant Azure
ff270c4b7d [ci skip] Translation update 2020-01-09 00:32:44 +00:00
David F. Mulcahey
5415068917 Rework the ZHA config panel (#4415)
* convert zha config panel to tabs

* add spacer to prevent combobox from hitting bottom

* break clusters out into their own section

* cleanup buttons

* remove header

* make devices default tab

* convert from tabs to a list view

* convert to table on dashboard

* fix anchor on mobile safari

* cleanup CSS to fix display on mobile

* cleanup card css

* more css cleanup

* fix group page

* remove translations changes

* Update src/panels/config/zha/zha-clusters.ts

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

* Update src/panels/config/zha/zha-config-dashboard.ts

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

* Update src/panels/config/zha/zha-device-page.ts

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

* Update src/panels/config/zha/zha-groups-dashboard.ts

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

* review comments

* fix dangling quote after commit suggestion

* css cleanup

* remove flex rules

* remove flex rules

* css  cleanup

* remove dialog per review comments

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-01-08 18:35:21 +01:00
Bram Kragten
357a67c00d Bumped version to 20200108.0 2020-01-08 18:26:20 +01:00
HomeAssistant Azure
cbe4269320 [ci skip] Translation update 2020-01-08 17:25:54 +00:00
Bram Kragten
fbd5185ce2 Add ability to remove Lovelace config (#4430)
* Add ability to remove Lovelace config

* Update hc-lovelace.ts
2020-01-08 18:19:10 +01:00
Bram Kragten
a33cf97e2c Fix moving actions with data (#4438) 2020-01-08 18:18:53 +01:00
Pascal Vizeli
7e7da26543 Update azure-pipelines-translation.yml for Azure Pipelines 2020-01-08 16:54:09 +01:00
Bram Kragten
79058e893b Add alert when Google sync failed (#4435) 2020-01-08 15:59:22 +01:00
Bram Kragten
2eb548bb74 Merge branch 'master' into dev 2020-01-07 20:53:23 +01:00
Bram Kragten
08baf8a757 Bumped version to 20200107.0 2020-01-07 20:50:51 +01:00
Bram Kragten
f02fa6a94b Add multi select to entity registry (#4424)
* Add multi select to entity registry

* Fix filter and sort on status

* Remove unused prop platform

* Review

* Update ha-config-entity-registry.ts
2020-01-07 12:29:42 +01:00
Bram Kragten
2ed6d0e73c Make modal of Lovelace editor dialogs (#4426)
Fixes #4425
2020-01-06 22:25:17 +01:00
David F. Mulcahey
35d9b2ac3c Add the ability to create new Zigbee groups to the ZHA config panel (#4384)
* add group page

* Update src/panels/config/zha/zha-add-group-page.ts

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

* fix group name handling

* Update src/panels/config/zha/zha-add-group-page.ts

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-01-06 07:02:47 -05:00
Bram Kragten
18d09c6f04 Add UI for restored entities (#4414)
* Add UI for restored entities

* Add conformation for removal

* Apply suggestions

* Guard
2020-01-03 12:44:25 +01:00
Joakim Sørensen
70b81de49d Force rerender on update/save (#4396)
* Force rerender on update/save

* Fix linting issue

* Define properties by using @property() instead

* Add styles to disabled save button

* Change to use @customElement, and remove _generation as a property.
2020-01-02 21:15:26 +01:00
David Cramer
f0808c1f54 Add ha-subppage toolbar css styles (#4409) 2020-01-02 20:55:43 +01:00
Jay
e779f0747e Change TRIGGER to EXECUTE (#4413)
There's been some confusion among new users about what the `TRIGGER` button does in the automation info popup. `EXECUTE` better represents what pressing that button does since it bypasses conditions and simply runs the action like a script. The automation docs at <https://www.home-assistant.io/docs/automation/action/> also say "The action of an automation rule is what is being executed when a rule fires."
2020-01-02 20:16:39 +01:00
David F. Mulcahey
bdd18775c3 Add group editing to the ZHA config panel (#4382)
* add group editing

* Update src/panels/config/zha/zha-devices-data-table.ts

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

* Update src/panels/config/zha/zha-group-page.ts

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

* Update src/panels/config/zha/zha-devices-data-table.ts

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

* Update src/panels/config/zha/zha-group-page.ts

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

* Update src/panels/config/zha/zha-group-page.ts

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

* Update src/panels/config/zha/zha-group-page.ts

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

* review comments

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-01-02 15:59:18 +01:00
David F. Mulcahey
711d51c022 Disable ZHA device binding buttons when a device to bind isn't selected (#4407)
* only enable buttons when a device is selected

* review comments
2020-01-02 07:24:40 -05:00
David F. Mulcahey
1b0d8bba29 fix area index on ZHA device card (#4406) 2020-01-02 10:50:19 +01:00
Colin Frei
2988cc512f Fix grammatical error (#4403) 2020-01-02 10:28:36 +01:00
Joakim Sørensen
a2f8e5f3e7 Hide protection mode toggle if not usable (#4392) 2020-01-02 10:20:21 +01:00
David F. Mulcahey
680bf06a4b Add group detail view to the ZHA config panel (#4380)
* add group details

* review comments
2019-12-24 10:29:22 -05:00
David F. Mulcahey
ff0b1881e2 Add Zigbee group removal to the ZHA config panel (#4376)
* add remove groups function

* add ability to remove groups

* translations

* review comments

* review comments

* review comments
2019-12-24 08:12:02 -05:00
David F. Mulcahey
de653e1f7b Add Zigbee group viewing to ZHA config panel (#4365)
* add ability to view zigbee groups

* review comments

* remove selectable until used
2019-12-23 10:46:34 -05:00
Bram Kragten
bb41170765 Add language Iban (#4375) 2019-12-23 16:27:41 +01:00
Bram Kragten
0ed2bc93aa Remove uploading translations from Travis (#4374) 2019-12-23 13:39:47 +01:00
Bram Kragten
04770f8ee2 Add language Esperanto (#4373) 2019-12-23 13:39:31 +01:00
Bram Kragten
15a2790b9f Add support to add all device entities to Lovelace (#4356)
* Add support to add all device entities to Lovelace

* Reload config when it was changed while Lovelace was not active

* Localize

* Update ha-panel-lovelace.ts

* Move to device entities card

* Move Lovelace logic to lovelace combine with unused entities

* Unused imports

* Added suggestions and support for YAML mode
2019-12-23 10:39:17 +01:00
Jc2k
83880791b1 Add 'unignore' to DISCOVERY_SOURCES that can be ignored. (#4370) 2019-12-21 17:10:20 +01:00
HomeAssistant Azure
4dca3289f6 [ci skip] Translation update 2019-12-19 16:07:21 +00:00
Pascal Vizeli
083a3ebfc4 Run translation on dev (#4368) 2019-12-19 17:03:05 +01:00
Pascal Vizeli
6117c4e989 Add Auto Translation handling (#4339)
* Add Auto Translation handling

* Cleanup
2019-12-18 16:38:36 +01:00
Bram Kragten
609763e658 Set focus to search when opening add integration dialog (#4357)
* Set focus to search when opening add integration dialog

* Also add to flow form
2019-12-18 16:35:20 +01:00
Bram Kragten
2c57ab60f1 Add ignore discovery button (#4354)
* Add ignore discovery button

* Add seperate list for ignored integrations

* Move translations

* Add zeroconf
2019-12-18 16:22:17 +01:00
Ian Richardson
dd17a153d2 Fire custom LL event (#4361) 2019-12-18 07:40:26 +01:00
Bram Kragten
c2d551bb7c Merge pull request #4341 from bonanitech/patch-2
Upgrade MDI icons to 4.7.95
2019-12-12 17:20:24 +01:00
Mauricio Bonani
e0b1921108 Fix version number 2019-12-09 12:40:11 -05:00
Mauricio Bonani
fcf39ceb96 Upgrade MDI icons to 4.7.95 2019-12-09 12:27:03 -05:00
Mauricio Bonani
3cc979a077 Upgrade MDI icons to 4.7.95 2019-12-09 12:24:36 -05:00
Bram Kragten
9972973774 Merge pull request #4338 from home-assistant/rc
20191204.1
2019-12-09 13:41:33 +01:00
Bram Kragten
20ae32bc26 Bumped version to 20191204.1 2019-12-09 13:03:17 +01:00
Bram Kragten
a29892023b Revert "Add copy entity ID/state/attributes menu button in dev tools/states" (#4337)
* Revert "Add copy entity ID/state/attributes menu button in dev tools/states (#4259)"

This reverts commit 4b56db5255.

* Update package.json
2019-12-09 13:02:41 +01:00
Bram Kragten
b283fec482 Update cloud-google-assistant.ts (#4329) 2019-12-09 13:02:17 +01:00
Bram Kragten
e0116a8236 Fix thingtalk automations creation (#4328) 2019-12-09 13:01:56 +01:00
Bram Kragten
d1990a4bac Revert "Add copy entity ID/state/attributes menu button in dev tools/states" (#4337)
* Revert "Add copy entity ID/state/attributes menu button in dev tools/states (#4259)"

This reverts commit 4b56db5255.

* Update package.json
2019-12-09 12:59:20 +01:00
Bram Kragten
cbba1849e2 Convert script and automation editor to lit (#4327)
* Convert script and automation editor to lit

* Update yarn.lock
2019-12-09 10:59:52 +01:00
Bram Kragten
43393d1647 Update cloud-google-assistant.ts (#4329) 2019-12-09 08:34:36 +01:00
Bram Kragten
b47ee1051c Fix thingtalk automations creation (#4328) 2019-12-07 20:46:04 +01:00
Bram Kragten
393adacc9e Convert automation actions/scripts to Lit (#4324)
* Convert automation actions/scripts to Lit

* Update ha-automation-action-row.ts

* Comments
2019-12-06 12:14:45 +01:00
Bram Kragten
073428849e Convert automation conditions to Lit (#4321)
* Convert automation conditions to Lit

* Split condition editor and row

* Comments

* Update automation.ts

* Update automation.ts
2019-12-05 19:48:06 +01:00
Bram Kragten
e6ac0258e3 Use dynamicElement directive in ha-form (#4317)
* Use dynamicContentDirective

* Turn around

* Remove attributes

* Rename to dynamicElement
2019-12-04 22:58:35 +01:00
Bram Kragten
d7e7798a55 Merge pull request #4318 from home-assistant/dev
20191204.0
2019-12-04 20:02:41 +01:00
Bram Kragten
2557414b11 Merge branch 'master' into dev 2019-12-04 19:30:47 +01:00
Bram Kragten
f7065fbce9 Bumped version to 20191204.0 2019-12-04 19:28:47 +01:00
Bram Kragten
016564eee9 Update translations 2019-12-04 19:22:23 +01:00
Bram Kragten
ff3087c39c Convert automation trigger to litelement (#4315)
* Convert automation trigger to Lit

* Update ha-automation-trigger-row.ts

* dynamicContentDirective

* update

* Lint

* Implement other types
2019-12-04 09:57:47 -08:00
Bram Kragten
239438ee5d Add entity picker to service call action (#4310)
* Add entity picker to service call action

* Use prop instead of attr
2019-12-03 12:30:51 +01:00
Florian Gareis
5458cda31f Add new confim dialog to automation editor (#4255) 2019-12-03 12:21:51 +01:00
Bram Kragten
36f49e66fd Remove empty defaults from time patern trigger automation (#4307) 2019-12-02 11:11:05 -08:00
Bram Kragten
2bafd38ea8 Allow automation actions/scripts to be moved up/down (#4308)
* Allow automation actions/scripts to be moved up/down

* Update index.tsx
2019-12-02 11:10:44 -08:00
Bram Kragten
73b3262491 Fix editing delay action (#4309) 2019-12-02 11:08:38 -08:00
Bram Kragten
808cde033f Update bug_report.md 2019-12-02 17:39:39 +01:00
Bram Kragten
fa8f6b7b91 Add yaml editor to automation actions and scripts (#4306)
* Add yaml editor to automation actions and scripts

* Add types

* Update event.tsx
2019-12-02 14:08:19 +01:00
Bram Kragten
94c120cdb1 Add yaml editor to automation conditions (#4305) 2019-12-02 12:02:35 +01:00
Bram Kragten
7b2be54f8f YAML support for automation triggers (#4289)
* WIP: Add yaml editors to automation

* Fix form overwriting yaml on switching back

* Finish triggers

* prettier
2019-12-02 11:20:09 +01:00
nicop4
4b56db5255 Add copy entity ID/state/attributes menu button in dev tools/states (#4259)
* Added button and js method to copy with copy-to-clipboard library

* Copy entity id working, tooltip added

* copy ok, use ha toast to notify ok

* cleanup code

* add translation

* removed old useless code

* Replaced copy button with menu

* Fix comparison operator & removed commented code

	modifié :         src/panels/developer-tools/state/developer-tools-state.js

* Fix spaces

	modifié :         src/panels/developer-tools/state/developer-tools-state.js

* Improve copy attributes

* only one menu & update translation

* copy attributes in yml format
use paper-icon-item instead of paper-icon-button and add yarn.lock

* removed paper-item
2019-12-02 10:35:49 +01:00
Bram Kragten
93165c9111 Area/multiple devices and name support for thingtalk automations (#4272)
* WIP: Area/multiple devices and name support

* Fix removing devices

* Don't recalc entities for all devices every time

* Use guards

* Update ha-thingtalk-placeholders.ts
2019-12-02 10:30:30 +01:00
Bram Kragten
caa604d5ca Add more aria labels (#4293)
* Add aria labels

* Fix polymer binding
2019-12-02 09:29:02 +01:00
Thomas Lovén
e7e9e2cf85 Allow setting temperature to 0 degrees (#4300) 2019-12-02 09:23:20 +01:00
Bram Kragten
daa04e9973 Fix jumping on iOS when toggle switch (#4275) 2019-11-29 12:41:37 +01:00
Bram Kragten
5355269f5d Check if external app by object (#4280)
* Check if external app by object

* Update core.ts

* Conditional chaining

* add babel optional chaining
2019-11-27 15:44:59 -08:00
Bram Kragten
2665a75250 Don't show hidden scenes (#4285)
* Don't show hidden scenes

* Comments

* computeStateDomain
2019-11-27 15:44:28 -08:00
Bram Kragten
8a39d18323 Bump TypeScript to 3.7 (#4282)
* Bump TypeScript to 3.7

* Update prettier to support ts 3.7

* Prettier

* More prettier

* Even more prettier
2019-11-27 13:51:03 -08:00
Bram Kragten
b8a026397b Don't filter attributes when saving scene (#4278)
* Add cover attributes to scene editor

* Add more

* Remove filtering of attributes

* Update ha-scene-editor.ts
2019-11-27 13:43:46 -08:00
Bram Kragten
bd5fe302eb Revert "Add specific maskable icons (#4283)" (#4284)
This reverts commit de0f1b2b65.
2019-11-27 20:23:58 +01:00
Bram Kragten
de0f1b2b65 Add specific maskable icons (#4283) 2019-11-27 16:43:23 +01:00
Thomas Lovén
defaa2b276 Fix missing semicolons in CSS (#4281)
Introduced in #4269
2019-11-27 13:06:02 +01:00
Bram Kragten
60efe00a1f Fix styling of vaadin elements (#4276) 2019-11-26 16:57:29 +01:00
Davide Varricchio
fe93b993db Change to thermostat card to reflect step_temp on set-temperature (#4221)
* Minor change to thermostat card to reflect step_temp on set-temperature

* Corrected indentation

* Resolved eslint error
2019-11-25 17:42:38 +01:00
Joakim Sørensen
f6afc92d3c Adds "air" at the bottom of the page (#4267)
* Adds "air" at the bottom of the page

* Update src/panels/config/dashboard/ha-config-dashboard.ts

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

* Add margin to promo
2019-11-25 17:38:08 +01:00
Carlos Gustavo Sarmiento
e4c635c855 Added new CSS property for styling of the app-header component (#4269) 2019-11-25 17:37:33 +01:00
Mauricio Bonani
8ef15c50b4 Upgrade MDI icons to 4.6.95 (#4270)
* Upgrade MDI icons to 4.6.95

* Upgrade MDI icons to 4.6.95
2019-11-23 21:26:32 +01:00
Marius
81588469b8 Add secondary-info: last-triggered (#4222)
* Add secondary-info: last-triggered

add last-triggered to the currently available options 'entity-id' and 'last-changed' see:https://www.home-assistant.io/lovelace/entities/#secondary_info

* corrected omission 'attributes'

* added test for attributes.last_triggered

* Update hui-generic-entity-row.ts

* Update hui-generic-entity-row.ts
2019-11-23 21:19:26 +01:00
Joakim Sørensen
70a920af3c Add initial bg color to panels (#4268) 2019-11-23 21:18:54 +01:00
Thomas Lovén
1329e60c89 Bump round-slider version. Fix #4265 (#4266) 2019-11-23 21:12:48 +01:00
Bram Kragten
ea9e8cc392 iOS 9 doesn't support append (#4260) 2019-11-21 17:03:35 +01:00
Bram Kragten
0acd41b7f0 Fix thermostat card (#4258)
* Fix thermostat card

* Change styling

* Remove margin on mode buttons
2019-11-21 15:18:16 +01:00
Bram Kragten
85ca73db84 Fix light card (#4257)
* Fix light card

* Remove unused class

* Fix for when entity is not available

* Fix active state
2019-11-21 15:17:55 +01:00
Bram Kragten
444cbd00d9 Update README.md 2019-11-21 15:05:42 +01:00
Thomas Lovén
6edf23b91f Version bump round-slider. Fix bad rendering in IE/Edge (#4249) 2019-11-20 10:55:06 +01:00
Bram Kragten
1249c0eea9 Fix ha-form on edge (#4248) 2019-11-19 21:06:52 +01:00
Bram Kragten
3133118870 Update vaadin components (#3571)
* Update vaadin components

* Remove resolution

* Migrate person detail dialog to mwc-dialog

* Fix imports

* Update dialog-person-detail.ts
2019-11-19 11:35:37 -06:00
377 changed files with 21510 additions and 7752 deletions

View File

@@ -41,7 +41,32 @@ Provide details about what browser (and version) you are seeing the issue in. An
**Description of problem:**
<!--
Explain what the issue is, and how things should look/behave. If possible provide a screenshot with a description.
Explain what the issue is, and what is the current behaviour. If possible provide a screenshot with a description.
-->
**Expected behaviour:**
<!--
Explain how things should look/behave. If possible provide a screenshot with a description.
-->
**Relevant config:**
<!--
Give the config of both the integration that is used, the Lovelace config, scene, automation or otherwise relevant configuration.
-->
**Steps to reproduce this problem:**
<!--
Sum up all steps that are necessary to reproduce this bug.
For example:
1. Add a climate integration
2. Navigate to Lovelace
3. Click more info of the climate entity
4. Set the hvac action to heat
5. Set the temperature higher than the current temperature
6. Set the hvac action to cool
-->
**Javascript errors shown in the web inspector (if applicable):**

27
.github/lock.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 1
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: 2020-01-01
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: false
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: false
# Limit to only `issues` or `pulls`
only: pulls
# Optionally, specify configuration settings just for `issues` or `pulls`
issues:
daysUntilLock: 30

View File

@@ -13,15 +13,6 @@ script:
- npm run test
# - xvfb-run wct --module-resolution=node --npm
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
services:
- docker
before_deploy:
- "docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21"
deploy:
provider: script
script: script/travis_deploy
"on":
branch: master
dist: trusty
addons:
sauce_connect: true

View File

@@ -2,9 +2,9 @@
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
[![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/home-assistant-polymer/master/docs/screenshot.png)](https://home-assistant.io/demo/)
[![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/home-assistant-polymer/master/docs/screenshot.png)](https://demo.home-assistant.io/)
- [View demo of the Polymer frontend](https://home-assistant.io/demo/)
- [View demo of Home Assistant](https://demo.home-assistant.io/)
- [More information about Home Assistant](https://home-assistant.io)
- [Frontend development instructions](https://developers.home-assistant.io/docs/en/frontend_index.html)
@@ -31,3 +31,5 @@ It is possible to compile the project and/or run commands in the development env
## License
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variation of devices.

View File

@@ -0,0 +1,70 @@
# https://dev.azure.com/home-assistant
trigger:
batch: true
branches:
include:
- dev
paths:
include:
- translations/en.json
pr: none
schedules:
- cron: "30 0 * * *"
displayName: "translation update"
branches:
include:
- dev
always: true
variables:
- group: translation
resources:
repositories:
- repository: azure
type: github
name: 'home-assistant/ci-azure'
endpoint: 'home-assistant'
jobs:
- job: 'Upload'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
displayName: 'Use Node 12.x'
inputs:
versionSpec: '12.x'
- script: |
export LOKALISE_TOKEN="$(lokaliseToken)"
export AZURE_BRANCH="$(Build.SourceBranchName)"
./script/translations_upload_base
displayName: 'Upload Translation'
- job: 'Download'
dependsOn:
- 'Upload'
condition: or(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual'))
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
displayName: 'Use Node 12.x'
inputs:
versionSpec: '12.x'
- template: templates/azp-step-git-init.yaml@azure
- script: |
export LOKALISE_TOKEN="$(lokaliseToken)"
export AZURE_BRANCH="$(Build.SourceBranchName)"
npm install
./script/translations_download
displayName: 'Download Translation'
- script: |
git checkout dev
git add translation
git commit -am "[ci skip] Translation update"
git push
displayName: 'Update translation'

View File

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

View File

@@ -91,7 +91,7 @@ const createWebpackConfig = ({
),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json", ".tsx"],
extensions: [".ts", ".js", ".json"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",

View File

@@ -39,6 +39,7 @@ class HcLovelace extends LitElement {
mode: "storage",
language: "en",
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
};
return this.lovelaceConfig.views[index].panel

View File

@@ -175,9 +175,9 @@ export class HcMain extends HassElement {
} catch (err) {
// Generate a Lovelace config.
this._unsubLovelace = () => undefined;
const {
generateLovelaceConfigFromHass,
} = await import("../../../../src/panels/lovelace/common/generate-lovelace-config");
const { generateLovelaceConfigFromHass } = await import(
"../../../../src/panels/lovelace/common/generate-lovelace-config"
);
this._handleNewLovelaceConfig(
await generateLovelaceConfigFromHass(this.hass!)
);

View File

@@ -53,7 +53,7 @@ class CardModder extends LitElement {
for (var k in this._config.style) {
if (window.cardTools.hasTemplate(this._config.style[k]))
this.templated.push(k);
this.card.style.setProperty(k, '');
this.card.style.setProperty(k, "");
target.style.setProperty(
k,
window.cardTools.parseTemplate(this._config.style[k])

View File

@@ -12,5 +12,7 @@ import "./resources/hademo-icons";
/* polyfill for paper-dropdown */
setTimeout(() => {
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
import(
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
);
}, 1000);

View File

@@ -65,74 +65,79 @@ const generateHistory = (state, deltas) => {
const incrementalUnits = ["clients", "queries", "ads"];
export const mockHistory = (mockHass: MockHomeAssistant) => {
mockHass.mockAPI(new RegExp("history/period/.+"), (
hass,
// @ts-ignore
method,
path,
// @ts-ignore
parameters
) => {
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
const entities = params.filter_entity_id.split(",");
mockHass.mockAPI(
new RegExp("history/period/.+"),
(
hass,
// @ts-ignore
method,
path,
// @ts-ignore
parameters
) => {
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
const entities = params.filter_entity_id.split(",");
const results: HassEntity[][] = [];
const results: HassEntity[][] = [];
for (const entityId of entities) {
const state = hass.states[entityId];
for (const entityId of entities) {
const state = hass.states[entityId];
if (!state) {
continue;
}
if (!state) {
continue;
}
if (!state.attributes.unit_of_measurement) {
results.push(generateHistory(state, [state.state]));
continue;
}
if (!state.attributes.unit_of_measurement) {
results.push(generateHistory(state, [state.state]));
continue;
}
const numberState = Number(state.state);
const numberState = Number(state.state);
if (isNaN(numberState)) {
// tslint:disable-next-line
console.log(
"Ignoring state with unparsable state but with a unit",
entityId,
state
if (isNaN(numberState)) {
// tslint:disable-next-line
console.log(
"Ignoring state with unparsable state but with a unit",
entityId,
state
);
continue;
}
const statesToGenerate = 15;
let genFunc;
if (incrementalUnits.includes(state.attributes.unit_of_measurement)) {
let initial = Math.floor(
numberState * 0.4 + numberState * Math.random() * 0.2
);
const diff = Math.max(
1,
Math.floor((numberState - initial) / statesToGenerate)
);
genFunc = () => {
initial += diff;
return Math.min(numberState, initial);
};
} else {
const diff = Math.floor(
numberState * (numberState > 80 ? 0.05 : 0.5)
);
genFunc = () =>
numberState - diff + Math.floor(Math.random() * 2 * diff);
}
results.push(
generateHistory(
{
entity_id: state.entity_id,
attributes: state.attributes,
},
Array.from({ length: statesToGenerate }, genFunc)
)
);
continue;
}
const statesToGenerate = 15;
let genFunc;
if (incrementalUnits.includes(state.attributes.unit_of_measurement)) {
let initial = Math.floor(
numberState * 0.4 + numberState * Math.random() * 0.2
);
const diff = Math.max(
1,
Math.floor((numberState - initial) / statesToGenerate)
);
genFunc = () => {
initial += diff;
return Math.min(numberState, initial);
};
} else {
const diff = Math.floor(numberState * (numberState > 80 ? 0.05 : 0.5));
genFunc = () =>
numberState - diff + Math.floor(Math.random() * 2 * diff);
}
results.push(
generateHistory(
{
entity_id: state.entity_id,
attributes: state.attributes,
},
Array.from({ length: statesToGenerate }, genFunc)
)
);
return results;
}
return results;
});
);
};

View File

@@ -12,9 +12,10 @@ export const mockLovelace = (
localizePromise: Promise<LocalizeFunc>
) => {
hass.mockWS("lovelace/config", () =>
Promise.all([selectedDemoConfig, localizePromise]).then(
([config, localize]) => config.lovelace(localize)
)
Promise.all([
selectedDemoConfig,
localizePromise,
]).then(([config, localize]) => config.lovelace(localize))
);
hass.mockWS("lovelace/config/save", () => Promise.resolve());

View File

@@ -44,9 +44,7 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
selected="{{selectedInput}}"
>
<template is="dom-repeat" items="[[inputDevices]]">
<paper-item device\$="[[item.device]]"
>[[item.name]]</paper-item
>
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
@@ -57,9 +55,7 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
selected="{{selectedOutput}}"
>
<template is="dom-repeat" items="[[outputDevices]]">
<paper-item device\$="[[item.device]]"
>[[item.name]]</paper-item
>
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>

View File

@@ -373,19 +373,21 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
</template>
</div>
</template>
<div class="state">
<div>
Protection mode
<span>
<iron-icon icon="hassio:information"></iron-icon>
<paper-tooltip>Grant the add-on elevated system access.</paper-tooltip>
</span>
<template is="dom-if" if="[[_computeUsesProtectedOptions(addon)]]">
<div class="state">
<div>
Protection mode
<span>
<iron-icon icon="hassio:information"></iron-icon>
<paper-tooltip>Grant the add-on elevated system access.</paper-tooltip>
</span>
</div>
<ha-switch
on-change="protectionToggled"
checked="[[addon.protected]]"
></ha-switch>
</div>
<ha-switch
on-change="protectionToggled"
checked="[[addon.protected]]"
></ha-switch>
</div>
</template>
</template>
</div>
<div class="card-actions">
@@ -569,7 +571,10 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
openChangelog() {
this.hass
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
.then((resp) => resp, () => "Error getting changelog")
.then(
(resp) => resp,
() => "Error getting changelog"
)
.then((content) => {
showHassioMarkdownDialog(this, {
title: "Changelog",
@@ -607,6 +612,10 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
return !addon.ingress || !this._computeHA92plus(hass);
}
_computeUsesProtectedOptions(addon) {
return addon.docker_api || addon.full_access || addon.host_pid;
}
_computeHA92plus(hass) {
const [major, minor] = hass.config.version.split(".", 2);
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);

View File

@@ -74,9 +74,7 @@ export class HassioUpdate extends LitElement {
this.supervisorInfo.version,
this.supervisorInfo.last_version,
"hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${
this.supervisorInfo.last_version
}`
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.last_version}`
)}
${this.hassOsInfo
? this._renderUpdateCard(
@@ -84,9 +82,7 @@ export class HassioUpdate extends LitElement {
this.hassOsInfo.version,
this.hassOsInfo.version_latest,
"hassio/hassos/update",
`https://github.com//home-assistant/hassos/releases/tag/${
this.hassOsInfo.version_latest
}`
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
)
: ""}
</div>

View File

@@ -12,7 +12,9 @@ export const showHassioMarkdownDialog = (
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-markdown",
dialogImport: () =>
import(/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"),
import(
/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"
),
dialogParams,
});
};

View File

@@ -12,7 +12,9 @@ export const showHassioSnapshotDialog = (
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-snapshot",
dialogImport: () =>
import(/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"),
import(
/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"
),
dialogParams,
});
};

View File

@@ -27,6 +27,7 @@ import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
// Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-pages-with-tabs";
import { navigate } from "../../src/common/navigate";
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
@@ -56,12 +57,16 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
addon: {
tag: "hassio-addon-view",
load: () =>
import(/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"),
import(
/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"
),
},
ingress: {
tag: "hassio-ingress-view",
load: () =>
import(/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"),
import(
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
),
},
},
};
@@ -161,14 +166,20 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
}),
]);
if (!addon.ingress_url) {
throw new Error("Add-on does not support Ingress");
alert("Add-on does not support Ingress");
return;
}
if (addon.state !== "started") {
alert("Add-on is not running. Please start it first");
navigate(this, `/hassio/addon/${addon.slug}`, true);
return;
}
location.assign(addon.ingress_url);
// await a promise that doesn't resolve, so we show the loading screen
// while we load the next page.
await new Promise(() => undefined);
} catch (err) {
alert(`Unable to open ingress connection `);
alert("Unable to open ingress connection");
}
}

View File

@@ -8,7 +8,7 @@
"version": "1.0.0",
"scripts": {
"build": "script/build_frontend",
"lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'src/**/*.tsx' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc",
"lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc",
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
"test": "npm run lint && npm run mocha",
"docker_build": "sh ./script/docker_run.sh build $npm_package_version",
@@ -19,13 +19,14 @@
"dependencies": {
"@material/chips": "^3.2.0",
"@material/data-table": "^3.2.0",
"@material/mwc-base": "^0.8.0",
"@material/mwc-button": "^0.8.0",
"@material/mwc-checkbox": "^0.8.0",
"@material/mwc-fab": "^0.8.0",
"@material/mwc-ripple": "^0.8.0",
"@material/mwc-switch": "^0.8.0",
"@mdi/svg": "4.5.95",
"@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",
"@mdi/svg": "4.7.95",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-localize-behavior": "^3.0.1",
"@polymer/app-route": "^3.0.2",
@@ -68,8 +69,8 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.3.7",
"@vaadin/vaadin-combo-box": "^4.2.8",
"@vaadin/vaadin-date-picker": "^3.3.3",
"@vaadin/vaadin-combo-box": "^5.0.6",
"@vaadin/vaadin-date-picker": "^4.0.3",
"@webcomponents/shadycss": "^1.9.0",
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
@@ -88,6 +89,7 @@
"leaflet": "^1.4.0",
"lit-element": "^2.2.1",
"lit-html": "^1.1.0",
"lit-virtualizer": "^0.4.2",
"marked": "^0.6.1",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
@@ -104,15 +106,16 @@
"xss": "^1.0.6"
},
"devDependencies": {
"@babel/core": "^7.4.0",
"@babel/plugin-external-helpers": "^7.2.0",
"@babel/plugin-proposal-class-properties": "^7.4.0",
"@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/plugin-proposal-object-rest-spread": "^7.4.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-react-jsx": "^7.3.0",
"@babel/preset-env": "^7.4.2",
"@babel/preset-typescript": "^7.4.0",
"@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",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1",
@@ -154,18 +157,18 @@
"merge-stream": "^1.0.1",
"mocha": "^6.0.2",
"parse5": "^5.1.0",
"prettier": "^1.16.4",
"prettier": "^1.19.1",
"raw-loader": "^2.0.0",
"reify": "^0.18.1",
"require-dir": "^1.2.0",
"sinon": "^7.3.1",
"terser-webpack-plugin": "^1.2.3",
"ts-mocha": "^6.0.0",
"tslint": "^5.14.0",
"tslint": "^5.20.1",
"tslint-config-prettier": "^1.18.0",
"tslint-eslint-rules": "^5.4.0",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "^3.6.3",
"typescript": "^3.7.2",
"web-component-tester": "^6.9.2",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9",
@@ -178,7 +181,6 @@
"_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569",
"resolutions": {
"@webcomponents/webcomponentsjs": "^2.2.10",
"@vaadin/vaadin-lumo-styles": "^1.4.2",
"@polymer/polymer": "3.1.0",
"lit-html": "^1.1.2"
},

View File

@@ -26,8 +26,8 @@ LANG_ISO=en
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "${CURRENT_BRANCH-}" != "master" ] && [ "${TRAVIS_BRANCH-}" != "master" ] ; then
echo "Please only run the translations upload script from a clean checkout of master."
if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${AZURE_BRANCH-}" != "dev" ] ; then
echo "Please only run the translations upload script from a clean checkout of dev."
exit 1
fi

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env bash
# Safe bash settings
# -e Exit on command fail
# -u Exit on unset variable
# -o pipefail Exit if piped command has error code
set -eu -o pipefail
cd "$(dirname "$0")/.."
script/translations_upload_base

View File

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

View File

@@ -98,9 +98,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
<ha-markdown
allowsvg
.content=${this.localize(
`ui.panel.page-authorize.form.providers.${
step.handler[0]
}.abort.${step.reason}`
`ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${step.reason}`
)}
></ha-markdown>
`;
@@ -229,9 +227,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
}
private _computeStepDescription(step: DataEntryFlowStepForm) {
const resourceKey = `ui.panel.page-authorize.form.providers.${
step.handler[0]
}.step.${step.step_id}.description`;
const resourceKey = `ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.description`;
const args: string[] = [];
const placeholders = step.description_placeholders || {};
Object.keys(placeholders).forEach((key) => {
@@ -245,9 +241,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
this.localize(
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${
step.step_id
}.data.${schema.name}`
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.data.${schema.name}`
);
}
@@ -255,9 +249,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
// Returns a callback for ha-form to calculate error messages
return (error) =>
this.localize(
`ui.panel.page-authorize.form.providers.${
step.handler[0]
}.error.${error}`
`ui.panel.page-authorize.form.providers.${step.handler[0]}.error.${error}`
);
}

View File

@@ -11,7 +11,9 @@ import "./ha-auth-flow";
import { AuthProvider, fetchAuthProviders } from "../data/auth";
import { registerServiceWorker } from "../util/register-service-worker";
import(/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider");
import(
/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider"
);
interface QueryParams {
client_id?: string;

View File

@@ -10,11 +10,11 @@ function toLocaleDateStringSupportsOptions() {
return false;
}
export default (toLocaleDateStringSupportsOptions()
export default 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, "mediumDate");

View File

@@ -10,7 +10,7 @@ function toLocaleStringSupportsOptions() {
return false;
}
export default (toLocaleStringSupportsOptions()
export default toLocaleStringSupportsOptions()
? (dateObj: Date, locales: string) =>
dateObj.toLocaleString(locales, {
year: "numeric",
@@ -19,4 +19,4 @@ export default (toLocaleStringSupportsOptions()
hour: "numeric",
minute: "2-digit",
})
: (dateObj: Date) => fecha.format(dateObj, "haDateTime"));
: (dateObj: Date) => fecha.format(dateObj, "haDateTime");

View File

@@ -10,10 +10,10 @@ function toLocaleTimeStringSupportsOptions() {
return false;
}
export default (toLocaleTimeStringSupportsOptions()
export default toLocaleTimeStringSupportsOptions()
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
hour: "numeric",
minute: "2-digit",
})
: (dateObj: Date) => fecha.format(dateObj, "shortTime"));
: (dateObj: Date) => fecha.format(dateObj, "shortTime");

View File

@@ -60,7 +60,7 @@ export const applyThemesOnElement = (
element.updateStyles(styles);
} else if (window.ShadyCSS) {
// implement updateStyles() method of Polymer elements
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ (element), styles);
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
}
if (!updateMeta) {

View File

@@ -0,0 +1,33 @@
import { directive, Part, NodePart } from "lit-html";
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"
);
}
let element = part.value as HTMLElement | undefined;
if (
element !== undefined &&
tag.toUpperCase() === (element as HTMLElement).tagName
) {
if (properties) {
Object.entries(properties).forEach(([key, value]) => {
element![key] = value;
});
}
return;
}
element = document.createElement(tag);
if (properties) {
Object.entries(properties).forEach(([key, value]) => {
element![key] = value;
});
}
part.setValue(element);
}
);

View File

@@ -11,7 +11,9 @@ export const setupLeafletMap = async (
throw new Error("Cannot setup Leaflet map on disconnected element");
}
// tslint:disable-next-line
const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet")) as LeafletModuleType;
const Leaflet = (await import(
/* webpackChunkName: "leaflet" */ "leaflet"
)) as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
const map = Leaflet.map(mapElement);

View File

@@ -1,25 +0,0 @@
// interface OnChangeComponent {
// props: {
// index: number;
// onChange(index: number, data: object);
// };
// }
// export function onChangeEvent(this: OnChangeComponent, prop, ev) {
export function onChangeEvent(this: any, prop, ev) {
const origData = this.props[prop];
if (ev.target.value === origData[ev.target.name]) {
return;
}
const data = { ...origData };
if (ev.target.value) {
data[ev.target.name] = ev.target.value;
} else {
delete data[ev.target.name];
}
this.props.onChange(this.props.index, data);
}

View File

@@ -1,9 +0,0 @@
import { render } from "preact";
export default function unmount(mountEl) {
render(
// @ts-ignore
() => null,
mountEl
);
}

View File

@@ -14,7 +14,11 @@ import "@material/mwc-button";
@customElement("search-input")
class SearchInput extends LitElement {
@property() private filter?: string;
@property() public filter?: string;
public focus() {
this.shadowRoot!.querySelector("paper-input")!.focus();
}
protected render(): TemplateResult | void {
return html`

View File

@@ -15,6 +15,7 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
id="progress"
progress="[[progress]]"
on-click="buttonTapped"
tabindex="0"
><slot></slot
></ha-progress-button>
`;

View File

@@ -6,8 +6,9 @@ import {
MDCDataTableFoundation,
} from "@material/data-table";
import { classMap } from "lit-html/directives/class-map";
import {
BaseElement,
html,
query,
queryAll,
@@ -15,10 +16,11 @@ import {
css,
customElement,
property,
classMap,
TemplateResult,
PropertyValues,
} from "@material/mwc-base/base-element";
} from "lit-element";
import { BaseElement } from "@material/mwc-base/base-element";
// eslint-disable-next-line import/no-webpack-loader-syntax
// @ts-ignore
@@ -73,7 +75,7 @@ export interface DataTableSortColumnData {
export interface DataTableColumnData extends DataTableSortColumnData {
title: string;
type?: "numeric" | "icon";
template?: <T>(data: any, row: T) => TemplateResult;
template?: <T>(data: any, row: T) => TemplateResult | string;
}
export interface DataTableRowData {
@@ -86,11 +88,11 @@ export class HaDataTable extends BaseElement {
@property({ type: Array }) public data: DataTableRowData[] = [];
@property({ type: Boolean }) public selectable = false;
@property({ type: String }) public id = "id";
@property({ type: String }) public filter = "";
protected mdcFoundation!: MDCDataTableFoundation;
protected readonly mdcFoundationClass = MDCDataTableFoundation;
@query(".mdc-data-table") protected mdcRoot!: HTMLElement;
@queryAll(".mdc-data-table__row") protected rowElements!: HTMLElement[];
@query("#header-checkbox") private _headerCheckbox!: HaCheckbox;
@property({ type: Boolean }) private _filterable = false;
@property({ type: Boolean }) private _headerChecked = false;
@property({ type: Boolean }) private _headerIndeterminate = false;
@@ -106,13 +108,19 @@ export class HaDataTable extends BaseElement {
private _worker: any | undefined;
private _debounceSearch = debounce(
(ev) => {
this._filter = ev.detail.value;
(value: string) => {
this._filter = value;
},
200,
false
);
public clearSelection(): void {
this._headerChecked = false;
this._headerIndeterminate = false;
this.mdcFoundation.handleHeaderRowCheckboxChange();
}
protected firstUpdated() {
super.firstUpdated();
this._worker = sortFilterWorker();
@@ -144,6 +152,10 @@ export class HaDataTable extends BaseElement {
this._sortColumns = clonedColumns;
}
if (properties.has("filter")) {
this._debounceSearch(this.filter);
}
if (
properties.has("data") ||
properties.has("columns") ||
@@ -157,14 +169,18 @@ export class HaDataTable extends BaseElement {
protected render() {
return html`
${this._filterable
? html`
<search-input
@value-changed=${this._handleSearchChange}
></search-input>
`
: ""}
<div class="mdc-data-table">
<slot name="header">
${this._filterable
? html`
<div class="table-header">
<search-input
@value-changed=${this._handleSearchChange}
></search-input>
</div>
`
: ""}
</slot>
<table class="mdc-data-table__table">
<thead>
<tr class="mdc-data-table__header-row">
@@ -176,7 +192,6 @@ export class HaDataTable extends BaseElement {
scope="col"
>
<ha-checkbox
id="header-checkbox"
class="mdc-data-table__row-checkbox"
@change=${this._handleHeaderRowCheckboxChange}
.indeterminate=${this._headerIndeterminate}
@@ -240,7 +255,9 @@ export class HaDataTable extends BaseElement {
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxChange}
.checked=${this._checkedRows.includes(row[this.id])}
.checked=${this._checkedRows.includes(
String(row[this.id])
)}
>
</ha-checkbox>
</td>
@@ -370,9 +387,10 @@ export class HaDataTable extends BaseElement {
});
}
private _handleHeaderRowCheckboxChange() {
this._headerChecked = this._headerCheckbox.checked;
this._headerIndeterminate = this._headerCheckbox.indeterminate;
private _handleHeaderRowCheckboxChange(ev: Event) {
const checkbox = ev.target as HaCheckbox;
this._headerChecked = checkbox.checked;
this._headerIndeterminate = checkbox.indeterminate;
this.mdcFoundation.handleHeaderRowCheckboxChange();
}
@@ -385,20 +403,26 @@ export class HaDataTable extends BaseElement {
}
private _handleRowClick(ev: Event) {
const rowId = (ev.target as HTMLElement)
.closest("tr")!
.getAttribute("data-row-id")!;
const target = ev.target as HTMLElement;
if (target.tagName === "HA-CHECKBOX") {
return;
}
const rowId = target.closest("tr")!.getAttribute("data-row-id")!;
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
}
private _setRowChecked(rowId: string, checked: boolean) {
if (checked && !this._checkedRows.includes(rowId)) {
this._checkedRows = [...this._checkedRows, rowId];
} else if (!checked) {
const index = this._checkedRows.indexOf(rowId);
if (index !== -1) {
this._checkedRows.splice(index, 1);
if (checked) {
if (this._checkedRows.includes(rowId)) {
return;
}
this._checkedRows = [...this._checkedRows, rowId];
} else {
const index = this._checkedRows.indexOf(rowId);
if (index === -1) {
return;
}
this._checkedRows.splice(index, 1);
}
fireEvent(this, "selection-changed", {
id: rowId,
@@ -407,7 +431,7 @@ export class HaDataTable extends BaseElement {
}
private _handleSearchChange(ev: CustomEvent): void {
this._debounceSearch(ev);
this._debounceSearch(ev.detail.value);
}
static get styles(): CSSResult {
@@ -587,6 +611,9 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__header-cell:hover.not-sorted ha-icon {
left: 0px;
}
.table-header {
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
`;
}
}

View File

@@ -0,0 +1,412 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import "@polymer/paper-listbox/paper-listbox";
import memoizeOne from "memoize-one";
import {
LitElement,
TemplateResult,
html,
css,
CSSResult,
customElement,
property,
PropertyValues,
} from "lit-element";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import "./ha-devices-picker";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { computeDomain } from "../../common/entity/compute_domain";
interface DevicesByArea {
[areaId: string]: AreaDevices;
}
interface AreaDevices {
id?: string;
name: string;
devices: string[];
}
const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: AreaDevices }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
width: 100%;
margin: -10px 0;
padding: 0;
}
paper-icon-button {
float: right;
}
.devices {
display: none;
}
.devices.visible {
display: block;
}
</style>
<paper-item>
<paper-item-body two-line="">
<div class='name'>[[item.name]]</div>
<div secondary>[[item.devices.length]] devices</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name!;
root.querySelector(
"[secondary]"
)!.textContent = `${model.item.devices.length.toString()} devices`;
};
@customElement("ha-area-devices-picker")
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public area?: string;
@property() public devices?: string[];
/**
* Show only devices with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no devices with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only deviced with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property({ type: Boolean })
private _opened?: boolean;
@property() private _areaPicker = true;
@property() private _devices?: DeviceRegistryEntry[];
@property() private _areas?: AreaRegistryEntry[];
@property() private _entities?: EntityRegistryEntry[];
private _selectedDevices: string[] = [];
private _filteredDevices: DeviceRegistryEntry[] = [];
private _getDevices = memoizeOne(
(
devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[],
entities: EntityRegistryEntry[],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"]
): AreaDevices[] => {
if (!devices.length) {
return [];
}
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
let inputDevices = [...devices];
if (includeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (excludeDomains) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
}
if (includeDeviceClasses) {
inputDevices = inputDevices.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
}
this._filteredDevices = inputDevices;
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
const devicesByArea: DevicesByArea = {};
for (const device of inputDevices) {
const areaId = device.area_id;
if (areaId) {
if (!(areaId in devicesByArea)) {
devicesByArea[areaId] = {
id: areaId,
name: areaLookup[areaId].name,
devices: [],
};
}
devicesByArea[areaId].devices.push(device.id);
}
}
const sorted = Object.keys(devicesByArea)
.sort((a, b) =>
compare(devicesByArea[a].name || "", devicesByArea[b].name || "")
)
.map((key) => devicesByArea[key]);
return sorted;
}
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices;
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = areas;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
}),
];
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("area") && this.area) {
this._areaPicker = true;
this.value = this.area;
} else if (changedProps.has("devices") && this.devices) {
this._areaPicker = false;
const filteredDeviceIds = this._filteredDevices.map(
(device) => device.id
);
const selectedDevices = this.devices.filter((device) =>
filteredDeviceIds.includes(device)
);
this._setValue(selectedDevices);
}
}
protected render(): TemplateResult | void {
if (!this._devices || !this._areas || !this._entities) {
return;
}
const areas = this._getDevices(
this._devices,
this._areas,
this._entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses
);
if (!this._areaPicker || areas.length === 0) {
return html`
<ha-devices-picker
@value-changed=${this._devicesPicked}
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.value=${this._selectedDevices}
.pickDeviceLabel=${`Add ${this.label} device`}
.pickedDeviceLabel=${`${this.label} device`}
></ha-devices-picker>
${areas.length > 0
? html`
<mwc-button @click=${this._switchPicker}
>Choose an area</mwc-button
>
`
: ""}
`;
}
return html`
<vaadin-combo-box-light
item-value-path="id"
item-id-path="id"
item-label-path="name"
.items=${areas}
.value=${this._value}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._areaPicked}
>
<paper-input
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: `${this.label} in area`}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<paper-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
Clear
</paper-icon-button>
`
: ""}
${areas.length > 0
? html`
<paper-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</paper-icon-button>
`
: ""}
</paper-input>
</vaadin-combo-box-light>
<mwc-button @click=${this._switchPicker}
>Choose individual devices</mwc-button
>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue([]);
}
private get _value() {
return this.value || [];
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private async _switchPicker() {
this._areaPicker = !this._areaPicker;
}
private async _areaPicked(ev: PolymerChangedEvent<string>) {
const value = ev.detail.value;
let selectedDevices = [];
const target = ev.target as any;
if (target.selectedItem) {
selectedDevices = target.selectedItem.devices;
}
if (value !== this._value || this._selectedDevices !== selectedDevices) {
this._setValue(selectedDevices, value);
}
}
private _devicesPicked(ev: CustomEvent) {
ev.stopPropagation();
const selectedDevices = ev.detail.value;
this._setValue(selectedDevices);
}
private _setValue(selectedDevices: string[], value = "") {
this.value = value;
this._selectedDevices = selectedDevices;
setTimeout(() => {
fireEvent(this, "value-changed", { value: selectedDevices });
fireEvent(this, "change");
}, 0);
}
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-area-devices-picker": HaAreaDevicesPicker;
}
}

View File

@@ -176,6 +176,7 @@ export abstract class HaDeviceAutomationPicker<
this.value = automation;
setTimeout(() => {
fireEvent(this, "change");
fireEvent(this, "value-changed", { value: automation });
}, 0);
}

View File

@@ -1,7 +1,7 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import "@polymer/paper-listbox/paper-listbox";
import memoizeOne from "memoize-one";
import {

View File

@@ -0,0 +1,127 @@
import {
LitElement,
TemplateResult,
property,
html,
customElement,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button-light";
import { HomeAssistant } from "../../types";
import { PolymerChangedEvent } from "../../polymer-types";
import { fireEvent } from "../../common/dom/fire_event";
import "./ha-device-picker";
@customElement("ha-devices-picker")
class HaDevicesPicker extends LitElement {
@property() public hass?: HomeAssistant;
@property() public value?: string[];
/**
* Show entities from specific domains.
* @type {string}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
@property({ attribute: "picked-device-label" })
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
public pickedDeviceLabel?: string;
@property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string;
protected render(): TemplateResult | void {
if (!this.hass) {
return;
}
const currentDevices = this._currentDevices;
return html`
${currentDevices.map(
(entityId) => html`
<div>
<ha-device-picker
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.value=${entityId}
.label=${this.pickedDeviceLabel}
@value-changed=${this._deviceChanged}
></ha-device-picker>
</div>
`
)}
<div>
<ha-device-picker
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.label=${this.pickDeviceLabel}
@value-changed=${this._addDevice}
></ha-device-picker>
</div>
`;
}
private get _currentDevices() {
return this.value || [];
}
private async _updateDevices(devices) {
fireEvent(this, "value-changed", {
value: devices,
});
this.value = devices;
}
private _deviceChanged(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const curValue = (event.currentTarget as any).curValue;
const newValue = event.detail.value;
if (newValue === curValue || newValue !== "") {
return;
}
if (newValue === "") {
this._updateDevices(
this._currentDevices.filter((dev) => dev !== curValue)
);
} else {
this._updateDevices(
this._currentDevices.map((dev) => (dev === curValue ? newValue : dev))
);
}
}
private async _addDevice(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const toAdd = event.detail.value;
(event.currentTarget as any).value = "";
if (!toAdd) {
return;
}
const currentDevices = this._currentDevices;
if (currentDevices.includes(toAdd)) {
return;
}
this._updateDevices([...currentDevices, toAdd]);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-devices-picker": HaDevicesPicker;
}
}

View File

@@ -215,7 +215,9 @@ class HaChartBase extends mixinBehaviors(
}
if (scriptsLoaded === null) {
scriptsLoaded = import(/* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js");
scriptsLoaded = import(
/* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js"
);
}
scriptsLoaded.then((ChartModule) => {
this.ChartClass = ChartModule.default;

View File

@@ -2,7 +2,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import memoizeOne from "memoize-one";
import "./state-badge";

View File

@@ -1,91 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import hassAttributeUtil from "../util/hass-attributes-util";
class HaAttributes extends PolymerElement {
static get template() {
return html`
<style include="iron-flex iron-flex-alignment"></style>
<style>
.data-entry .value {
max-width: 200px;
overflow-wrap: break-word;
}
.attribution {
color: var(--secondary-text-color);
text-align: right;
}
</style>
<div class="layout vertical">
<template
is="dom-repeat"
items="[[computeDisplayAttributes(stateObj, filtersArray)]]"
as="attribute"
>
<div class="data-entry layout justified horizontal">
<div class="key">[[formatAttribute(attribute)]]</div>
<div class="value">
[[formatAttributeValue(stateObj, attribute)]]
</div>
</div>
</template>
<div class="attribution" hidden$="[[!computeAttribution(stateObj)]]">
[[computeAttribution(stateObj)]]
</div>
</div>
`;
}
static get properties() {
return {
stateObj: {
type: Object,
},
extraFilters: {
type: String,
value: "",
},
filtersArray: {
type: Array,
computed: "computeFiltersArray(extraFilters)",
},
};
}
computeFiltersArray(extraFilters) {
return Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat(
extraFilters ? extraFilters.split(",") : []
);
}
computeDisplayAttributes(stateObj, filtersArray) {
if (!stateObj) {
return [];
}
return Object.keys(stateObj.attributes).filter(function(key) {
return filtersArray.indexOf(key) === -1;
});
}
formatAttribute(attribute) {
return attribute.replace(/_/g, " ");
}
formatAttributeValue(stateObj, attribute) {
var value = stateObj.attributes[attribute];
if (value === null) return "-";
if (Array.isArray(value)) {
return value.join(", ");
}
return value instanceof Object ? JSON.stringify(value, null, 2) : value;
}
computeAttribution(stateObj) {
return stateObj.attributes.attribution;
}
}
customElements.define("ha-attributes", HaAttributes);

View File

@@ -0,0 +1,97 @@
import {
property,
LitElement,
TemplateResult,
html,
CSSResult,
css,
customElement,
} from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import hassAttributeUtil from "../util/hass-attributes-util";
@customElement("ha-attributes")
class HaAttributes extends LitElement {
@property() public stateObj?: HassEntity;
@property() public extraFilters?: string;
protected render(): TemplateResult | void {
if (!this.stateObj) {
return html``;
}
return html`
<div>
${this.computeDisplayAttributes(
Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat(
this.extraFilters ? this.extraFilters.split(",") : []
)
).map(
(attribute) => html`
<div class="data-entry">
<div class="key">${attribute.replace(/_/g, " ")}</div>
<div class="value">
${this.formatAttributeValue(attribute)}
</div>
</div>
`
)}
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
${this.stateObj.attributes.attribution}
</div>
`
: ""}
</div>
`;
}
static get styles(): CSSResult {
return css`
.data-entry {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.data-entry .value {
max-width: 200px;
overflow-wrap: break-word;
}
.attribution {
color: var(--secondary-text-color);
text-align: right;
}
`;
}
private computeDisplayAttributes(filtersArray: string[]): string[] {
if (!this.stateObj) {
return [];
}
return Object.keys(this.stateObj.attributes).filter((key) => {
return filtersArray.indexOf(key) === -1;
});
}
private formatAttributeValue(attribute: string): string {
if (!this.stateObj) {
return "-";
}
const value = this.stateObj.attributes[attribute];
if (value === null) {
return "-";
}
if (Array.isArray(value)) {
return value.join(", ");
}
return value instanceof Object ? JSON.stringify(value, null, 2) : value;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-attributes": HaAttributes;
}
}

View File

@@ -122,8 +122,9 @@ class HaCameraStream extends LitElement {
private async _startHls(): Promise<void> {
// tslint:disable-next-line
const Hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
.default as HLSModule;
const Hls = ((await import(
/* webpackChunkName: "hls.js" */ "hls.js"
)) as any).default as HLSModule;
let hlsSupported = Hls.isSupported();
const videoEl = this._videoEl;

View File

@@ -72,9 +72,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
computeCurrentStatus(hass, stateObj) {
if (!hass || !stateObj) return null;
if (stateObj.attributes.current_temperature != null) {
return `${stateObj.attributes.current_temperature} ${
hass.config.unit_system.temperature
}`;
return `${stateObj.attributes.current_temperature} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.current_humidity != null) {
return `${stateObj.attributes.current_humidity} %`;
@@ -89,22 +87,16 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null
) {
return `${stateObj.attributes.target_temp_low}-${
stateObj.attributes.target_temp_high
} ${hass.config.unit_system.temperature}`;
return `${stateObj.attributes.target_temp_low}-${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.temperature != null) {
return `${stateObj.attributes.temperature} ${
hass.config.unit_system.temperature
}`;
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
}
if (
stateObj.attributes.target_humidity_low != null &&
stateObj.attributes.target_humidity_high != null
) {
return `${stateObj.attributes.target_humidity_low}-${
stateObj.attributes.target_humidity_high
}%`;
return `${stateObj.attributes.target_humidity_low}-${stateObj.attributes.target_humidity_high}%`;
}
if (stateObj.attributes.humidity != null) {
return `${stateObj.attributes.humidity} %`;
@@ -121,9 +113,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
const stateString = localize(`state.climate.${stateObj.state}`);
return stateObj.attributes.hvac_action
? `${localize(
`state_attributes.climate.hvac_action.${
stateObj.attributes.hvac_action
}`
`state_attributes.climate.hvac_action.${stateObj.attributes.hvac_action}`
)} (${stateString})`
: stateString;
}

View File

@@ -3,7 +3,7 @@ import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { EventsMixin } from "../mixins/events-mixin";

View File

@@ -1,4 +1,5 @@
import { classMap, html, customElement } from "@material/mwc-base/base-element";
import { classMap } from "lit-html/directives/class-map";
import { html, customElement } from "lit-element";
import { ripple } from "@material/mwc-ripple/ripple-directive.js";
import "@material/mwc-fab";

View File

@@ -3,10 +3,8 @@ import {
LitElement,
html,
property,
query,
CSSResult,
css,
PropertyValues,
} from "lit-element";
import "./ha-form-string";
@@ -16,6 +14,7 @@ import "./ha-form-boolean";
import "./ha-form-select";
import "./ha-form-positive_time_period_dict";
import { fireEvent } from "../../common/dom/fire_event";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
export type HaFormSchema =
| HaFormStringSchema
@@ -100,20 +99,14 @@ export class HaForm extends LitElement implements HaFormElement {
@property() public computeError?: (schema: HaFormSchema, error) => string;
@property() public computeLabel?: (schema: HaFormSchema) => string;
@property() public computeSuffix?: (schema: HaFormSchema) => string;
@query("ha-form") private _childForm?: HaForm;
@query("#element") private _elementContainer?: HTMLDivElement;
public focus() {
const input = this._childForm
? this._childForm
: this._elementContainer
? this._elementContainer.lastChild
: undefined;
const input =
this.shadowRoot!.getElementById("child-form") ||
this.shadowRoot!.querySelector("ha-form");
if (!input) {
return;
}
(input as HTMLElement).focus();
}
@@ -151,40 +144,16 @@ export class HaForm extends LitElement implements HaFormElement {
</div>
`
: ""}
<div id="element"></div>
${dynamicElement(`ha-form-${this.schema.type}`, {
schema: this.schema,
data: this.data,
label: this._computeLabel(this.schema),
suffix: this._computeSuffix(this.schema),
id: "child-form",
})}
`;
}
protected updated(changedProperties: PropertyValues) {
const schemaChanged = changedProperties.has("schema");
const oldSchema = schemaChanged
? changedProperties.get("schema")
: undefined;
if (
!Array.isArray(this.schema) &&
schemaChanged &&
(!oldSchema || (oldSchema as HaFormSchema).type !== this.schema.type)
) {
const element = document.createElement(
`ha-form-${this.schema.type}`
) as HaFormElement;
element.schema = this.schema;
element.data = this.data;
element.label = this._computeLabel(this.schema);
element.suffix = this._computeSuffix(this.schema);
if (this._elementContainer!.lastChild) {
this._elementContainer!.removeChild(this._elementContainer!.lastChild);
}
this._elementContainer!.appendChild(element);
} else if (this._elementContainer && this._elementContainer.lastChild) {
const element = this._elementContainer!.lastChild as HaFormElement;
element.schema = this.schema;
element.data = this.data;
element.label = this._computeLabel(this.schema);
element.suffix = this._computeSuffix(this.schema);
}
}
private _computeLabel(schema: HaFormSchema) {
return this.computeLabel
? this.computeLabel(schema)

View File

@@ -520,10 +520,13 @@ class HaSidebar extends LitElement {
}
a {
text-decoration: none;
color: var(--sidebar-text-color);
font-weight: 500;
font-size: 14px;
text-decoration: none;
position: relative;
display: block;
outline: 0;
}
paper-icon-item {
@@ -546,7 +549,8 @@ class HaSidebar extends LitElement {
color: var(--sidebar-icon-color);
}
.iron-selected paper-icon-item:before {
.iron-selected paper-icon-item::before,
a:not(.iron-selected):focus::before {
border-radius: 4px;
position: absolute;
top: 0;
@@ -555,11 +559,22 @@ class HaSidebar extends LitElement {
left: 0;
pointer-events: none;
content: "";
background-color: var(--sidebar-selected-icon-color);
opacity: 0.12;
transition: opacity 15ms linear;
will-change: opacity;
}
.iron-selected paper-icon-item::before {
background-color: var(--sidebar-selected-icon-color);
opacity: 0.12;
}
a:not(.iron-selected):focus::before {
background-color: currentColor;
opacity: var(--dark-divider-opacity);
margin: 4px 8px;
}
.iron-selected paper-icon-item:focus::before,
.iron-selected:focus paper-icon-item::before {
opacity: 0.2;
}
.iron-selected paper-icon-item[pressed]:before {
opacity: 0.37;

View File

@@ -58,14 +58,10 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null
) {
return `${stateObj.attributes.target_temp_low} - ${
stateObj.attributes.target_temp_high
} ${hass.config.unit_system.temperature}`;
return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.temperature != null) {
return `${stateObj.attributes.temperature} ${
hass.config.unit_system.temperature
}`;
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
}
return "";

View File

@@ -0,0 +1,96 @@
import { safeDump, safeLoad } from "js-yaml";
import "./ha-code-editor";
import { LitElement, property, customElement, html, query } from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import { afterNextRender } from "../common/util/render-status";
// tslint:disable-next-line
import { HaCodeEditor } from "./ha-code-editor";
const isEmpty = (obj: object) => {
if (typeof obj !== "object") {
return false;
}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
return false;
}
}
return true;
};
@customElement("ha-yaml-editor")
export class HaYamlEditor extends LitElement {
@property() public value?: any;
@property() public isValid = true;
@property() public label?: string;
@property() private _yaml?: string;
@query("ha-code-editor") private _editor?: HaCodeEditor;
public setValue(value) {
try {
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
} catch (err) {
alert(`There was an error converting to YAML: ${err}`);
}
afterNextRender(() => {
if (this._editor?.codemirror) {
this._editor.codemirror.refresh();
}
});
}
protected firstUpdated() {
this.setValue(this.value);
}
protected render() {
if (this._yaml === undefined) {
return;
}
return html`
${this.label
? html`
<p>${this.label}</p>
`
: ""}
<ha-code-editor
.value=${this._yaml}
mode="yaml"
.error=${this.isValid === false}
@value-changed=${this._onChange}
></ha-code-editor>
`;
}
private _onChange(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value;
let parsed;
let isValid = true;
if (value) {
try {
parsed = safeLoad(value);
isValid = true;
} catch (err) {
// Invalid YAML
isValid = false;
}
} else {
parsed = {};
}
this.value = parsed;
this.isValid = isValid;
if (isValid) {
fireEvent(this, "value-changed", { value: parsed });
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-yaml-editor": HaYamlEditor;
}
}

View File

@@ -4,6 +4,8 @@ import {
} from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { navigate } from "../common/navigate";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action } from "./script";
export interface AutomationEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & {
@@ -15,11 +17,162 @@ export interface AutomationEntity extends HassEntityBase {
export interface AutomationConfig {
alias: string;
description: string;
trigger: any[];
condition?: any[];
action: any[];
trigger: Trigger[];
condition?: Condition[];
action: Action[];
}
export interface ForDict {
hours?: number | string;
minutes?: number | string;
seconds?: number | string;
}
export interface StateTrigger {
platform: "state";
entity_id?: string;
from?: string | number;
to?: string | number;
for?: string | number | ForDict;
}
export interface MqttTrigger {
platform: "mqtt";
topic: string;
payload?: string;
}
export interface GeoLocationTrigger {
platform: "geo_location";
source: "string";
zone: "string";
event: "enter" | "leave";
}
export interface HassTrigger {
platform: "homeassistant";
event: "start" | "shutdown";
}
export interface NumericStateTrigger {
platform: "numeric_state";
entity_id: string;
above?: number;
below?: number;
value_template?: string;
for?: string | number | ForDict;
}
export interface SunTrigger {
platform: "sun";
offset: number;
event: "sunrise" | "sunset";
}
export interface TimePatternTrigger {
platform: "time_pattern";
hours?: number | string;
minutes?: number | string;
seconds?: number | string;
}
export interface WebhookTrigger {
platform: "webhook";
webhook_id: string;
}
export interface ZoneTrigger {
platform: "zone";
entity_id: string;
zone: string;
event: "enter" | "leave";
}
export interface TimeTrigger {
platform: "time";
at: string;
}
export interface TemplateTrigger {
platform: "template";
value_template: string;
}
export interface EventTrigger {
platform: "event";
event_type: string;
event_data: any;
}
export type Trigger =
| StateTrigger
| MqttTrigger
| GeoLocationTrigger
| HassTrigger
| NumericStateTrigger
| SunTrigger
| TimePatternTrigger
| WebhookTrigger
| ZoneTrigger
| TimeTrigger
| TemplateTrigger
| EventTrigger
| DeviceTrigger;
export interface LogicalCondition {
condition: "and" | "or";
conditions: Condition[];
}
export interface StateCondition {
condition: "state";
entity_id: string;
state: string | number;
}
export interface NumericStateCondition {
condition: "numeric_state";
entity_id: string;
above?: number;
below?: number;
value_template?: string;
}
export interface SunCondition {
condition: "sun";
after_offset: number;
before_offset: number;
after: "sunrise" | "sunset";
before: "sunrise" | "sunset";
}
export interface ZoneCondition {
condition: "zone";
entity_id: string;
zone: string;
}
export interface TimeCondition {
condition: "time";
after: string;
before: string;
}
export interface TemplateCondition {
condition: "template";
value_template: string;
}
export type Condition =
| StateCondition
| NumericStateCondition
| SunCondition
| ZoneCondition
| TimeCondition
| TemplateCondition
| DeviceCondition
| LogicalCondition;
export const deleteAutomation = (hass: HomeAssistant, id: string) =>
hass.callApi("DELETE", `config/automation/config/${id}`);

View File

@@ -19,9 +19,7 @@ export interface Stream {
}
export const computeMJPEGStreamUrl = (entity: CameraEntity) =>
`/api/camera_proxy_stream/${entity.entity_id}?token=${
entity.attributes.access_token
}`;
`/api/camera_proxy_stream/${entity.entity_id}?token=${entity.attributes.access_token}`;
export const fetchThumbnailUrlWithCache = (
hass: HomeAssistant,

View File

@@ -4,6 +4,8 @@ import { debounce } from "../common/util/debounce";
import { getCollection, Connection } from "home-assistant-js-websocket";
import { LocalizeFunc } from "../common/translations/localize";
export const DISCOVERY_SOURCES = ["unignore", "homekit", "ssdp", "zeroconf"];
export const createConfigFlow = (hass: HomeAssistant, handler: string) =>
hass.callApi<DataEntryFlowStep>("POST", "config/config_entries/flow", {
handler,
@@ -26,6 +28,9 @@ export const handleConfigFlowStep = (
data
);
export const ignoreConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callWS({ type: "config_entries/ignore_flow", flow_id: flowId });
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);

View File

@@ -3,7 +3,7 @@ import { HomeAssistant } from "../types";
interface ProcessResults {
card: { [key: string]: { [key: string]: string } };
speech: {
[SpeechType in "plain" | "ssml"]: { extra_data: any; speech: string }
[SpeechType in "plain" | "ssml"]: { extra_data: any; speech: string };
};
}

View File

@@ -18,7 +18,7 @@ export interface DeviceCondition extends DeviceAutomation {
}
export interface DeviceTrigger extends DeviceAutomation {
platform: string;
platform: "device";
}
export const fetchDeviceActions = (hass: HomeAssistant, deviceId: string) =>
@@ -107,9 +107,7 @@ export const localizeDeviceAutomationAction = (
state ? computeStateName(state) : "<unknown>",
"subtype",
hass.localize(
`component.${action.domain}.device_automation.action_subtype.${
action.subtype
}`
`component.${action.domain}.device_automation.action_subtype.${action.subtype}`
)
);
};
@@ -122,16 +120,12 @@ export const localizeDeviceAutomationCondition = (
? hass.states[condition.entity_id]
: undefined;
return hass.localize(
`component.${condition.domain}.device_automation.condition_type.${
condition.type
}`,
`component.${condition.domain}.device_automation.condition_type.${condition.type}`,
"entity_name",
state ? computeStateName(state) : "<unknown>",
"subtype",
hass.localize(
`component.${condition.domain}.device_automation.condition_subtype.${
condition.subtype
}`
`component.${condition.domain}.device_automation.condition_subtype.${condition.subtype}`
)
);
};
@@ -142,16 +136,12 @@ export const localizeDeviceAutomationTrigger = (
) => {
const state = trigger.entity_id ? hass.states[trigger.entity_id] : undefined;
return hass.localize(
`component.${trigger.domain}.device_automation.trigger_type.${
trigger.type
}`,
`component.${trigger.domain}.device_automation.trigger_type.${trigger.type}`,
"entity_name",
state ? computeStateName(state) : "<unknown>",
"subtype",
hass.localize(
`component.${trigger.domain}.device_automation.trigger_subtype.${
trigger.subtype
}`
`component.${trigger.domain}.device_automation.trigger_subtype.${trigger.subtype}`
)
);
};

View File

@@ -149,9 +149,7 @@ export const createHassioSession = async (hass: HomeAssistant) => {
"POST",
"hassio/ingress/session"
);
document.cookie = `ingress_session=${
response.data.session
};path=/api/hassio_ingress/`;
document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`;
};
export const reloadHassioAddons = (hass: HomeAssistant) =>

7
src/data/logbook.ts Normal file
View File

@@ -0,0 +1,7 @@
export interface LogbookEntry {
when: string;
name: string;
message: string;
entity_id?: string;
domain: string;
}

View File

@@ -69,6 +69,10 @@ export interface NoActionConfig extends BaseActionConfig {
action: "none";
}
export interface CustomActionConfig extends BaseActionConfig {
action: "fire-dom-event";
}
export interface BaseActionConfig {
confirmation?: ConfirmationRestrictionConfig;
}
@@ -88,7 +92,8 @@ export type ActionConfig =
| NavigateActionConfig
| UrlActionConfig
| MoreInfoActionConfig
| NoActionConfig;
| NoActionConfig
| CustomActionConfig;
export const fetchConfig = (
conn: Connection,
@@ -108,6 +113,11 @@ export const saveConfig = (
config,
});
export const deleteConfig = (hass: HomeAssistant): Promise<void> =>
hass.callWS({
type: "lovelace/config/delete",
});
export const subscribeLovelaceUpdates = (
conn: Connection,
onChange: () => void

View File

@@ -18,38 +18,6 @@ export const SCENE_IGNORED_DOMAINS = [
"zone",
];
export const SCENE_SAVED_ATTRIBUTES = {
light: [
"brightness",
"color_temp",
"effect",
"rgb_color",
"xy_color",
"hs_color",
],
media_player: [
"is_volume_muted",
"volume_level",
"sound_mode",
"source",
"media_content_id",
"media_content_type",
],
climate: [
"target_temperature",
"target_temperature_high",
"target_temperature_low",
"target_humidity",
"fan_mode",
"swing_mode",
"hvac_mode",
"preset_mode",
],
vacuum: ["cleaning_mode"],
fan: ["speed", "current_direction"],
water_heather: ["temperature", "operation_mode"],
};
export interface SceneEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & { id?: string };
}

View File

@@ -1,5 +1,21 @@
import { HomeAssistant } from "../types";
import { computeObjectId } from "../common/entity/compute_object_id";
import { Condition } from "./automation";
import {
HassEntityBase,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
export interface ScriptEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & {
last_triggered: string;
};
}
export interface ScriptConfig {
alias: string;
sequence: Action[];
}
export interface EventAction {
event: string;
@@ -7,12 +23,40 @@ export interface EventAction {
event_data_template?: { [key: string]: any };
}
export interface ServiceAction {
service: string;
entity_id?: string;
data?: { [key: string]: any };
}
export interface DeviceAction {
device_id: string;
domain: string;
entity_id: string;
}
export interface DelayAction {
delay: number;
}
export interface SceneAction {
scene: string;
}
export interface WaitAction {
wait_template: string;
timeout?: number;
}
export type Action =
| EventAction
| DeviceAction
| ServiceAction
| Condition
| DelayAction
| SceneAction
| WaitAction;
export const triggerScript = (
hass: HomeAssistant,
entityId: string,

View File

@@ -51,6 +51,12 @@ export interface ReadAttributeServiceData {
manufacturer?: number;
}
export interface ZHAGroup {
name: string;
group_id: number;
members: ZHADevice[];
}
export const reconfigureNode = (
hass: HomeAssistant,
ieeeAddress: string
@@ -120,6 +126,32 @@ export const unbindDevices = (
target_ieee: targetIEEE,
});
export const bindDeviceToGroup = (
hass: HomeAssistant,
deviceIEEE: string,
groupId: number,
clusters: Cluster[]
): Promise<void> =>
hass.callWS({
type: "zha/groups/bind",
source_ieee: deviceIEEE,
group_id: groupId,
bindings: clusters,
});
export const unbindDeviceFromGroup = (
hass: HomeAssistant,
deviceIEEE: string,
groupId: number,
clusters: Cluster[]
): Promise<void> =>
hass.callWS({
type: "zha/groups/unbind",
source_ieee: deviceIEEE,
group_id: groupId,
bindings: clusters,
});
export const readAttributeValue = (
hass: HomeAssistant,
data: ReadAttributeServiceData
@@ -153,3 +185,66 @@ export const fetchClustersForZhaNode = (
type: "zha/devices/clusters",
ieee: ieeeAddress,
});
export const fetchGroups = (hass: HomeAssistant): Promise<ZHAGroup[]> =>
hass.callWS({
type: "zha/groups",
});
export const removeGroups = (
hass: HomeAssistant,
groupIdsToRemove: number[]
): Promise<ZHAGroup[]> =>
hass.callWS({
type: "zha/group/remove",
group_ids: groupIdsToRemove,
});
export const fetchGroup = (
hass: HomeAssistant,
groupId: number
): Promise<ZHAGroup> =>
hass.callWS({
type: "zha/group",
group_id: groupId,
});
export const fetchGroupableDevices = (
hass: HomeAssistant
): Promise<ZHADevice[]> =>
hass.callWS({
type: "zha/devices/groupable",
});
export const addMembersToGroup = (
hass: HomeAssistant,
groupId: number,
membersToAdd: string[]
): Promise<ZHAGroup> =>
hass.callWS({
type: "zha/group/members/add",
group_id: groupId,
members: membersToAdd,
});
export const removeMembersFromGroup = (
hass: HomeAssistant,
groupId: number,
membersToRemove: string[]
): Promise<ZHAGroup> =>
hass.callWS({
type: "zha/group/members/remove",
group_id: groupId,
members: membersToRemove,
});
export const addGroup = (
hass: HomeAssistant,
groupName: string,
membersToAdd?: string[]
): Promise<ZHAGroup> =>
hass.callWS({
type: "zha/group/add",
group_name: groupName,
members: membersToAdd,
});

View File

@@ -98,9 +98,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
"ui.dialogs.config_entry_system_options.enable_new_entities_description",
"integration",
this.hass.localize(
`component.${
this._params.entry.domain
}.config.title`
`component.${this._params.entry.domain}.config.title`
) || this._params.entry.domain
)}
</p>
@@ -117,7 +115,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
.disabled=${this._submitting}
>
${this.hass.localize(
"ui.panel.config.entity_registry.editor.update"
"ui.panel.config.entities.editor.update"
)}
</mwc-button>
</div>

View File

@@ -10,7 +10,9 @@ export interface ConfigEntrySystemOptionsDialogParams {
}
export const loadConfigEntrySystemOptionsDialog = () =>
import(/* webpackChunkName: "config-entry-system-options" */ "./dialog-config-entry-system-options");
import(
/* webpackChunkName: "config-entry-system-options" */ "./dialog-config-entry-system-options"
);
export const showConfigEntrySystemOptionsDialog = (
element: HTMLElement,

View File

@@ -11,6 +11,7 @@ import {
import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
@@ -124,6 +125,7 @@ class DataEntryFlowDialog extends LitElement {
<ha-paper-dialog
with-backdrop
opened
modal
@opened-changed=${this._openedChanged}
>
${this._loading || (this._step === null && this._handlers === undefined)
@@ -134,53 +136,62 @@ class DataEntryFlowDialog extends LitElement {
? // When we are going to next step, we render 1 round of empty
// to reset the element.
""
: this._step === null
? // Show handler picker
html`
<step-flow-pick-handler
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
></step-flow-pick-handler>
`
: this._step.type === "form"
? html`
<step-flow-form
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-form>
`
: this._step.type === "external"
? html`
<step-flow-external
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-external>
`
: this._step.type === "abort"
? html`
<step-flow-abort
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-abort>
`
: this._devices === undefined || this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry
html`
<step-flow-loading></step-flow-loading>
`
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.devices=${this._devices}
.areas=${this._areas}
></step-flow-create-entry>
<paper-icon-button
aria-label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.dismiss"
)}
icon="hass:close"
dialog-dismiss
></paper-icon-button>
${this._step === null
? // Show handler picker
html`
<step-flow-pick-handler
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
></step-flow-pick-handler>
`
: this._step.type === "form"
? html`
<step-flow-form
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-form>
`
: this._step.type === "external"
? html`
<step-flow-external
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-external>
`
: this._step.type === "abort"
? html`
<step-flow-abort
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-abort>
`
: this._devices === undefined || this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry
html`
<step-flow-loading></step-flow-loading>
`
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.devices=${this._devices}
.areas=${this._areas}
></step-flow-create-entry>
`}
`}
</ha-paper-dialog>
`;
@@ -318,6 +329,12 @@ class DataEntryFlowDialog extends LitElement {
display: block;
padding: 0;
}
paper-icon-button {
display: inline-block;
padding: 8px;
margin: 16px 16px 0 0;
float: right;
}
`,
];
}

View File

@@ -71,9 +71,7 @@ export const showConfigFlowDialog = (
renderShowFormStepFieldLabel(hass, step, field) {
return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.data.${
field.name
}`
`component.${step.handler}.config.step.${step.step_id}.data.${field.name}`
);
},

View File

@@ -79,7 +79,9 @@ export interface DataEntryFlowDialogParams {
}
export const loadDataEntryFlowDialog = () =>
import(/* webpackChunkName: "dialog-config-flow" */ "./dialog-data-entry-flow");
import(
/* webpackChunkName: "dialog-config-flow" */ "./dialog-data-entry-flow"
);
export const showFlowDialog = (
element: HTMLElement,

View File

@@ -54,9 +54,7 @@ export const showOptionsFlowDialog = (
renderShowFormStepFieldLabel(hass, step, field) {
return hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.data.${
field.name
}`
`component.${configEntry.domain}.options.step.${step.step_id}.data.${field.name}`
);
},

View File

@@ -109,6 +109,7 @@ class StepFlowForm extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0);
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
this._submitStep();

View File

@@ -19,6 +19,7 @@ import "../../components/ha-icon-next";
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";
interface HandlerObj {
name: string;
@@ -101,6 +102,14 @@ class StepFlowPickHandler extends LitElement {
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
setTimeout(
() => this.shadowRoot!.querySelector("search-input")!.focus(),
0
);
}
protected updated(changedProps) {
super.updated(changedProps);
// Store the width so that when we search, box doesn't jump
@@ -125,28 +134,27 @@ class StepFlowPickHandler extends LitElement {
});
}
static get styles(): CSSResult {
return css`
h2 {
margin-bottom: 2px;
padding-left: 16px;
}
div {
overflow: auto;
max-height: 600px;
}
paper-item {
cursor: pointer;
}
p {
text-align: center;
padding: 16px;
margin: 0;
}
p > a {
color: var(--primary-color);
}
`;
static get styles(): CSSResult[] {
return [
configFlowContentStyles,
css`
div {
overflow: auto;
max-height: 600px;
}
paper-item {
cursor: pointer;
}
p {
text-align: center;
padding: 16px;
margin: 0;
}
p > a {
color: var(--primary-color);
}
`,
];
}
}

View File

@@ -117,9 +117,7 @@ class DialogDeviceRegistryDetail extends LitElement {
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<mwc-button @click="${this._updateEntry}">
${this.hass.localize(
"ui.panel.config.entity_registry.editor.update"
)}
${this.hass.localize("ui.panel.config.entities.editor.update")}
</mwc-button>
</div>
</ha-paper-dialog>

View File

@@ -12,7 +12,9 @@ export interface DeviceRegistryDetailDialogParams {
}
export const loadDeviceRegistryDetailDialog = () =>
import(/* webpackChunkName: "device-registry-detail-dialog" */ "./dialog-device-registry-detail");
import(
/* webpackChunkName: "device-registry-detail-dialog" */ "./dialog-device-registry-detail"
);
export const showDeviceRegistryDetailDialog = (
element: HTMLElement,

View File

@@ -6,7 +6,9 @@ export interface HaDomainTogglerDialogParams {
}
export const loadDomainTogglerDialog = () =>
import(/* webpackChunkName: "dialog-domain-toggler" */ "./dialog-domain-toggler");
import(
/* webpackChunkName: "dialog-domain-toggler" */ "./dialog-domain-toggler"
);
export const showDomainTogglerDialog = (
element: HTMLElement,

View File

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

View File

@@ -2,7 +2,7 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@vaadin/vaadin-date-picker/vaadin-date-picker";
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker";
import "../../../components/ha-relative-time";
import "../../../components/paper-time-input";

View File

@@ -1,6 +1,7 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import "@material/mwc-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -11,18 +12,25 @@ import "../../state-summary/state-card-content";
import "./controls/more-info-content";
import { navigate } from "../../common/navigate";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
import { EventsMixin } from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
import { computeRTL } from "../../common/util/compute_rtl";
import { removeEntityRegistryEntry } from "../../data/entity_registry";
import { showConfirmationDialog } from "../confirmation/show-dialog-confirmation";
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
const EDITABLE_DOMAINS = ["script"];
/*
* @appliesMixin EventsMixin
*/
class MoreInfoControls extends EventsMixin(PolymerElement) {
class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="ha-style-dialog">
@@ -56,6 +64,10 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
padding-bottom: 16px;
}
mwc-button.warning {
--mdc-theme-primary: var(--google-red-500);
}
:host([domain="camera"]) paper-dialog-scrollable {
margin: 0 -24px -21px;
}
@@ -68,7 +80,7 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
<app-toolbar>
<paper-icon-button
aria-label="Dismiss dialog"
aria-label$="[[localize('ui.dialogs.more_info_control.dismiss')]]"
icon="hass:close"
dialog-dismiss
></paper-icon-button>
@@ -77,10 +89,18 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
</div>
<template is="dom-if" if="[[canConfigure]]">
<paper-icon-button
aria-label$="[[localize('ui.dialogs.more_info_control.settings')]]"
icon="hass:settings"
on-click="_gotoSettings"
></paper-icon-button>
</template>
<template is="dom-if" if="[[_computeEdit(hass, stateObj)]]">
<paper-icon-button
aria-label$="[[localize('ui.dialogs.more_info_control.edit')]]"
icon="hass:pencil"
on-click="_gotoEdit"
></paper-icon-button>
</template>
</app-toolbar>
<template is="dom-if" if="[[_computeShowStateInfo(stateObj)]]" restamp="">
@@ -115,6 +135,15 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
state-obj="[[stateObj]]"
hass="[[hass]]"
></more-info-content>
<template
is="dom-if"
if="[[_computeShowRestored(stateObj)]]"
restamp=""
>
[[localize('ui.dialogs.more_info_control.restored.not_provided')]] <br />
[[localize('ui.dialogs.more_info_control.restored.remove_intro')]] <br />
<mwc-button class="warning" on-click="_removeEntity">[[localize('ui.dialogs.more_info_control.restored.remove_action')]]</mwc-buttom>
</template>
</paper-dialog-scrollable>
`;
}
@@ -170,6 +199,10 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
return !stateObj || !DOMAINS_NO_INFO.includes(computeStateDomain(stateObj));
}
_computeShowRestored(stateObj) {
return stateObj && stateObj.attributes.restored;
}
_computeShowHistoryComponent(hass, stateObj) {
return (
hass &&
@@ -187,6 +220,16 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
return stateObj ? computeStateName(stateObj) : "";
}
_computeEdit(hass, stateObj) {
const domain = this._computeDomain(stateObj);
return (
stateObj &&
hass.user.is_admin &&
((EDITABLE_DOMAINS_WITH_ID.includes(domain) && stateObj.attributes.id) ||
EDITABLE_DOMAINS.includes(domain))
);
}
_stateObjChanged(newVal) {
if (!newVal) {
return;
@@ -200,10 +243,38 @@ class MoreInfoControls extends EventsMixin(PolymerElement) {
}
}
_removeEntity() {
showConfirmationDialog(this, {
title: this.localize(
"ui.dialogs.more_info_control.restored.confirm_remove_title"
),
text: this.localize(
"ui.dialogs.more_info_control.restored.confirm_remove_text"
),
confirmBtnText: this.localize("ui.common.yes"),
cancelBtnText: this.localize("ui.common.no"),
confirm: () =>
removeEntityRegistryEntry(this.hass, this.stateObj.entity_id),
});
}
_gotoSettings() {
this.fire("more-info-page", { page: "settings" });
}
_gotoEdit() {
const domain = this._computeDomain(this.stateObj);
navigate(
this,
`/config/${domain}/edit/${
EDITABLE_DOMAINS_WITH_ID.includes(domain)
? this.stateObj.attributes.id
: this.stateObj.entity_id
}`
);
this.fire("hass-more-info", { entityId: null });
}
_computeRTL(hass) {
return computeRTL(hass);
}

View File

@@ -47,6 +47,7 @@ class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
<app-toolbar>
<ha-paper-icon-button-arrow-prev
aria-label$="[[localize('ui.dialogs.more_info_settings.back')]]"
on-click="_backTapped"
></ha-paper-icon-button-arrow-prev>
<div main-title="">[[_computeStateName(stateObj)]]</div>

View File

@@ -49,10 +49,10 @@ export class HuiNotificationDrawer extends EventsMixin(
text-align: center;
}
</style>
<app-drawer id='drawer' opened="{{open}}" disable-swipe align="start">
<app-drawer id="drawer" opened="{{open}}" disable-swipe align="start">
<app-toolbar>
<div main-title>[[localize('ui.notification_drawer.title')]]</div>
<ha-paper-icon-button-prev on-click="_closeDrawer"></paper-icon-button>
<ha-paper-icon-button-prev on-click="_closeDrawer" aria-label$="[[localize('ui.notification_drawer.close')]]"></paper-icon-button>
</app-toolbar>
<div class="notifications">
<template is="dom-if" if="[[!_empty(notifications)]]">

View File

@@ -1,7 +1,9 @@
import { fireEvent } from "../../common/dom/fire_event";
const loadVoiceCommandDialog = () =>
import(/* webpackChunkName: "ha-voice-command-dialog" */ "./ha-voice-command-dialog");
import(
/* webpackChunkName: "ha-voice-command-dialog" */ "./ha-voice-command-dialog"
);
export const showVoiceCommandDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {

View File

@@ -54,9 +54,8 @@ class DialogZHADeviceInfo extends LitElement {
class="card"
.hass=${this.hass}
.device=${this._device}
showActions
isJoinPage
@zha-device-removed=${this._onDeviceRemoved}
.showEntityDetail=${false}
></zha-device-card>
`}
</ha-paper-dialog>

View File

@@ -5,7 +5,9 @@ export interface ZHADeviceInfoDialogParams {
}
export const loadZHADeviceInfoDialog = () =>
import(/* webpackChunkName: "dialog-zha-device-info" */ "./dialog-zha-device-info");
import(
/* webpackChunkName: "dialog-zha-device-info" */ "./dialog-zha-device-info"
);
export const showZHADeviceInfoDialog = (
element: HTMLElement,

View File

@@ -10,6 +10,8 @@ import "../auth/ha-authorize";
/* polyfill for paper-dropdown */
setTimeout(
() =>
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"),
import(
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
),
2000
);

View File

@@ -23,13 +23,16 @@ declare global {
}
}
const isExternal = location.search.includes("external_auth=1");
const isExternal =
window.externalApp ||
window.webkit?.messageHandlers?.getExternalAuth ||
location.search.includes("external_auth=1");
const authProm = isExternal
? () =>
import(/* webpackChunkName: "external_auth" */ "../external_app/external_auth").then(
({ createExternalAuth }) => createExternalAuth(hassUrl)
)
import(
/* webpackChunkName: "external_auth" */ "../external_app/external_auth"
).then(({ createExternalAuth }) => createExternalAuth(hassUrl))
: () =>
getAuth({
hassUrl,
@@ -52,8 +55,12 @@ const connProm = async (auth) => {
throw err;
}
// We can get invalid auth if auth tokens were stored that are no longer valid
// Clear stored tokens.
if (!isExternal) {
if (isExternal) {
// Tell the external app to force refresh the access tokens.
// This should trigger their unauthorized handling.
await auth.refreshAccessToken(true);
} else {
// Clear stored tokens.
saveTokens(null);
}
auth = await authProm();
@@ -63,6 +70,9 @@ const connProm = async (auth) => {
};
if (__DEV__) {
// Remove adoptedStyleSheets so style inspector works on shadow DOM.
// @ts-ignore
delete Document.prototype.adoptedStyleSheets;
performance.mark("hass-start");
}
window.hassConnection = authProm().then(connProm);

View File

@@ -11,6 +11,10 @@ interface BasePayload {
callback: string;
}
interface GetExternalAuthPayload extends BasePayload {
force?: boolean;
}
interface RefreshTokenResponse {
access_token: string;
expires_in: number;
@@ -26,7 +30,7 @@ declare global {
webkit?: {
messageHandlers: {
getExternalAuth: {
postMessage(payload: BasePayload);
postMessage(payload: GetExternalAuthPayload);
};
revokeExternalAuth: {
postMessage(payload: BasePayload);
@@ -60,8 +64,13 @@ class ExternalAuth extends Auth {
});
}
public async refreshAccessToken() {
const callbackPayload = { callback: CALLBACK_SET_TOKEN };
public async refreshAccessToken(force?: boolean) {
const payload: GetExternalAuthPayload = {
callback: CALLBACK_SET_TOKEN,
};
if (force) {
payload.force = true;
}
const callbackPromise = new Promise<RefreshTokenResponse>(
(resolve, reject) => {
@@ -73,11 +82,9 @@ class ExternalAuth extends Auth {
await 0;
if (window.externalApp) {
window.externalApp.getExternalAuth(JSON.stringify(callbackPayload));
window.externalApp.getExternalAuth(JSON.stringify(payload));
} else {
window.webkit!.messageHandlers.getExternalAuth.postMessage(
callbackPayload
);
window.webkit!.messageHandlers.getExternalAuth.postMessage(payload);
}
const tokens = await callbackPromise;
@@ -87,7 +94,7 @@ class ExternalAuth extends Auth {
}
public async revoke() {
const callbackPayload = { callback: CALLBACK_REVOKE_TOKEN };
const payload: BasePayload = { callback: CALLBACK_REVOKE_TOKEN };
const callbackPromise = new Promise((resolve, reject) => {
window[CALLBACK_REVOKE_TOKEN] = (success, data) =>
@@ -97,11 +104,9 @@ class ExternalAuth extends Auth {
await 0;
if (window.externalApp) {
window.externalApp.revokeExternalAuth(JSON.stringify(callbackPayload));
window.externalApp.revokeExternalAuth(JSON.stringify(payload));
} else {
window.webkit!.messageHandlers.revokeExternalAuth.postMessage(
callbackPayload
);
window.webkit!.messageHandlers.revokeExternalAuth.postMessage(payload);
}
await callbackPromise;

View File

@@ -74,20 +74,23 @@ export const provideHass = (
restResponses.push([path, callback]);
}
mockAPI(new RegExp("states/.+"), (
// @ts-ignore
method,
path,
parameters
) => {
const [domain, objectId] = path.substr(7).split(".", 2);
if (!domain || !objectId) {
return;
mockAPI(
new RegExp("states/.+"),
(
// @ts-ignore
method,
path,
parameters
) => {
const [domain, objectId] = path.substr(7).split(".", 2);
if (!domain || !objectId) {
return;
}
addEntities(
getEntity(domain, objectId, parameters.state, parameters.attributes)
);
}
addEntities(
getEntity(domain, objectId, parameters.state, parameters.attributes)
);
});
);
const localLanguage = getLocalLanguage();
@@ -117,9 +120,7 @@ export const provideHass = (
? callback(msg)
: Promise.reject({
code: "command_not_mocked",
message: `WS Command ${
msg.type
} is not implemented in provide_hass.`,
message: `WS Command ${msg.type} is not implemented in provide_hass.`,
});
},
subscribeMessage: async (onChange, msg) => {
@@ -128,9 +129,7 @@ export const provideHass = (
? callback(msg, onChange)
: Promise.reject({
code: "command_not_mocked",
message: `WS Command ${
msg.type
} is not implemented in provide_hass.`,
message: `WS Command ${msg.type} is not implemented in provide_hass.`,
});
},
subscribeEvents: async (

View File

@@ -9,12 +9,14 @@ import {
} from "lit-element";
import "../components/ha-menu-button";
import "../components/ha-paper-icon-button-arrow-prev";
import { classMap } from "lit-html/directives/class-map";
@customElement("hass-subpage")
class HassSubpage extends LitElement {
@property()
public header?: string;
@property({ type: Boolean })
public showBackButton = true;
@property({ type: Boolean })
public hassio = false;
@@ -25,6 +27,7 @@ class HassSubpage extends LitElement {
aria-label="Back"
.hassio=${this.hassio}
@click=${this._backTapped}
class=${classMap({ hidden: !this.showBackButton })}
></ha-paper-icon-button-arrow-prev>
<div main-title>${this.header}</div>
@@ -53,9 +56,9 @@ class HassSubpage extends LitElement {
height: 64px;
padding: 0 16px;
pointer-events: none;
background-color: var(--primary-color);
background-color: var(--app-header-background-color);
font-weight: 400;
color: var(--text-primary-color, white);
color: var(--app-header-text-color, white);
}
ha-menu-button,
@@ -64,6 +67,10 @@ class HassSubpage extends LitElement {
pointer-events: auto;
}
ha-paper-icon-button-arrow-prev.hidden {
visibility: hidden;
}
[main-title] {
margin: 0 0 0 24px;
line-height: 20px;

View File

@@ -45,7 +45,9 @@ export class HomeAssistantAppEl extends HassElement {
this._initialize();
setTimeout(registerServiceWorker, 1000);
/* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
import(
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
);
}
protected updated(changedProps: PropertyValues): void {
@@ -55,9 +57,10 @@ export class HomeAssistantAppEl extends HassElement {
this._updateHass({ panelUrl: this._panelUrl });
}
if (changedProps.has("hass")) {
this.hassChanged(this.hass!, changedProps.get("hass") as
| HomeAssistant
| undefined);
this.hassChanged(
this.hass!,
changedProps.get("hass") as HomeAssistant | undefined
);
}
}

View File

@@ -12,33 +12,59 @@ import { removeInitSkeleton } from "../util/init-skeleton";
const CACHE_COMPONENTS = ["lovelace", "states", "developer-tools"];
const COMPONENTS = {
calendar: () =>
import(/* webpackChunkName: "panel-calendar" */ "../panels/calendar/ha-panel-calendar"),
import(
/* webpackChunkName: "panel-calendar" */ "../panels/calendar/ha-panel-calendar"
),
config: () =>
import(/* webpackChunkName: "panel-config" */ "../panels/config/ha-panel-config"),
import(
/* webpackChunkName: "panel-config" */ "../panels/config/ha-panel-config"
),
custom: () =>
import(/* webpackChunkName: "panel-custom" */ "../panels/custom/ha-panel-custom"),
import(
/* webpackChunkName: "panel-custom" */ "../panels/custom/ha-panel-custom"
),
"developer-tools": () =>
import(/* webpackChunkName: "panel-developer-tools" */ "../panels/developer-tools/ha-panel-developer-tools"),
import(
/* webpackChunkName: "panel-developer-tools" */ "../panels/developer-tools/ha-panel-developer-tools"
),
lovelace: () =>
import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"),
import(
/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"
),
states: () =>
import(/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states"),
import(
/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states"
),
history: () =>
import(/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history"),
import(
/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history"
),
iframe: () =>
import(/* webpackChunkName: "panel-iframe" */ "../panels/iframe/ha-panel-iframe"),
import(
/* webpackChunkName: "panel-iframe" */ "../panels/iframe/ha-panel-iframe"
),
kiosk: () =>
import(/* webpackChunkName: "panel-kiosk" */ "../panels/kiosk/ha-panel-kiosk"),
import(
/* webpackChunkName: "panel-kiosk" */ "../panels/kiosk/ha-panel-kiosk"
),
logbook: () =>
import(/* webpackChunkName: "panel-logbook" */ "../panels/logbook/ha-panel-logbook"),
import(
/* webpackChunkName: "panel-logbook" */ "../panels/logbook/ha-panel-logbook"
),
mailbox: () =>
import(/* webpackChunkName: "panel-mailbox" */ "../panels/mailbox/ha-panel-mailbox"),
import(
/* webpackChunkName: "panel-mailbox" */ "../panels/mailbox/ha-panel-mailbox"
),
map: () =>
import(/* webpackChunkName: "panel-map" */ "../panels/map/ha-panel-map"),
profile: () =>
import(/* webpackChunkName: "panel-profile" */ "../panels/profile/ha-panel-profile"),
import(
/* webpackChunkName: "panel-profile" */ "../panels/profile/ha-panel-profile"
),
"shopping-list": () =>
import(/* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list"),
import(
/* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list"
),
};
const getRoutes = (panels: Panels): RouterOptions => {

View File

@@ -84,8 +84,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._fetchOnboardingSteps();
import(/* webpackChunkName: "onboarding-integrations" */ "./onboarding-integrations");
import(/* webpackChunkName: "onboarding-core-config" */ "./onboarding-core-config");
import(
/* webpackChunkName: "onboarding-integrations" */ "./onboarding-integrations"
);
import(
/* webpackChunkName: "onboarding-core-config" */ "./onboarding-core-config"
);
registerServiceWorker(false);
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
}

View File

@@ -116,9 +116,13 @@ class OnboardingCoreConfig extends LitElement {
@value-changed=${this._handleChange}
>
<span slot="suffix">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)}
${this._unitSystem === "metric"
? this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)
: this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_feet"
)}
</span>
</paper-input>
</div>
@@ -266,7 +270,7 @@ class OnboardingCoreConfig extends LitElement {
});
} catch (err) {
this._working = false;
alert("FAIL");
alert(`Failed to save: ${err.message}`);
}
}

View File

@@ -124,7 +124,9 @@ class OnboardingIntegrations extends LitElement {
loadConfigFlowDialog();
this._loadConfigEntries();
/* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
import(
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
);
}
private _createFlow() {

View File

@@ -47,9 +47,7 @@ class DialogAreaDetail extends LitElement {
<h2>
${entry
? entry.name
: this.hass.localize(
"ui.panel.config.area_registry.editor.default_name"
)}
: this.hass.localize("ui.panel.config.areas.editor.default_name")}
</h2>
<paper-dialog-scrollable>
${this._error
@@ -81,9 +79,7 @@ class DialogAreaDetail extends LitElement {
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
${this.hass.localize(
"ui.panel.config.area_registry.editor.delete"
)}
${this.hass.localize("ui.panel.config.areas.editor.delete")}
</mwc-button>
`
: html``}
@@ -92,12 +88,8 @@ class DialogAreaDetail extends LitElement {
.disabled=${nameInvalid || this._submitting}
>
${entry
? this.hass.localize(
"ui.panel.config.area_registry.editor.update"
)
: this.hass.localize(
"ui.panel.config.area_registry.editor.create"
)}
? this.hass.localize("ui.panel.config.areas.editor.update")
: this.hass.localize("ui.panel.config.areas.editor.create")}
</mwc-button>
</div>
</ha-paper-dialog>

View File

@@ -5,6 +5,7 @@ import {
css,
CSSResult,
property,
customElement,
} from "lit-element";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
@@ -30,7 +31,8 @@ import { classMap } from "lit-html/directives/class-map";
import { computeRTL } from "../../../common/util/compute_rtl";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
class HaConfigAreaRegistry extends LitElement {
@customElement("ha-config-areas")
export class HaConfigAreas extends LitElement {
@property() public hass!: HomeAssistant;
@property() public isWide?: boolean;
@property() private _areas?: AreaRegistryEntry[];
@@ -51,24 +53,23 @@ class HaConfigAreaRegistry extends LitElement {
}
return html`
<hass-subpage
header="${this.hass.localize("ui.panel.config.area_registry.caption")}"
.header="${this.hass.localize("ui.panel.config.areas.caption")}"
.showBackButton=${!this.isWide}
>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize("ui.panel.config.area_registry.picker.header")}
${this.hass.localize("ui.panel.config.areas.picker.header")}
</span>
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.area_registry.picker.introduction"
)}
${this.hass.localize("ui.panel.config.areas.picker.introduction")}
<p>
${this.hass.localize(
"ui.panel.config.area_registry.picker.introduction2"
"ui.panel.config.areas.picker.introduction2"
)}
</p>
<a href="/config/integrations/dashboard">
${this.hass.localize(
"ui.panel.config.area_registry.picker.integrations_page"
"ui.panel.config.areas.picker.integrations_page"
)}
</a>
</span>
@@ -85,13 +86,9 @@ class HaConfigAreaRegistry extends LitElement {
${this._areas.length === 0
? html`
<div class="empty">
${this.hass.localize(
"ui.panel.config.area_registry.no_areas"
)}
${this.hass.localize("ui.panel.config.areas.no_areas")}
<mwc-button @click=${this._createArea}>
${this.hass.localize(
"ui.panel.config.area_registry.create_area"
)}
${this.hass.localize("ui.panel.config.areas.create_area")}
</mwc-button>
</div>
`
@@ -103,9 +100,7 @@ class HaConfigAreaRegistry extends LitElement {
<ha-fab
?is-wide=${this.isWide}
icon="hass:plus"
title="${this.hass.localize(
"ui.panel.config.area_registry.create_area"
)}"
title="${this.hass.localize("ui.panel.config.areas.create_area")}"
@click=${this._createArea}
class="${classMap({
rtl: computeRTL(this.hass),
@@ -208,5 +203,3 @@ All devices in this area will become unassigned.`)
`;
}
}
customElements.define("ha-config-area-registry", HaConfigAreaRegistry);

View File

@@ -14,7 +14,9 @@ export interface AreaRegistryDetailDialogParams {
}
export const loadAreaRegistryDetailDialog = () =>
import(/* webpackChunkName: "area-registry-detail-dialog" */ "./dialog-area-registry-detail");
import(
/* webpackChunkName: "area-registry-detail-dialog" */ "./dialog-area-registry-detail"
);
export const showAreaRegistryDetailDialog = (
element: HTMLElement,

View File

@@ -0,0 +1,272 @@
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
// tslint:disable-next-line
import { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
import { HomeAssistant } from "../../../../types";
import { Action } from "../../../../data/script";
import "./types/ha-automation-action-service";
import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-event";
import "./types/ha-automation-action-condition";
import "./types/ha-automation-action-scene";
import "./types/ha-automation-action-wait_template";
const OPTIONS = [
"condition",
"delay",
"device_id",
"event",
"scene",
"service",
"wait_template",
];
const getType = (action: Action) => {
return OPTIONS.find((option) => option in action);
};
declare global {
// for fire event
interface HASSDomEvents {
"move-action": { direction: "up" | "down" };
}
}
export interface ActionElement extends LitElement {
action: Action;
}
export const handleChangeEvent = (element: ActionElement, ev: CustomEvent) => {
ev.stopPropagation();
const name = (ev.target as any)?.name;
if (!name) {
return;
}
const newVal = ev.detail.value;
if ((element.action[name] || "") === newVal) {
return;
}
let newAction: Action;
if (!newVal) {
newAction = { ...element.action };
delete newAction[name];
} else {
newAction = { ...element.action, [name]: newVal };
}
fireEvent(element, "value-changed", { value: newAction });
};
@customElement("ha-automation-action-row")
export default class HaAutomationActionRow extends LitElement {
@property() public hass!: HomeAssistant;
@property() public action!: Action;
@property() public index!: number;
@property() public totalActions!: number;
@property() private _yamlMode = false;
protected render() {
const type = getType(this.action);
const selected = type ? OPTIONS.indexOf(type) : -1;
const yamlMode = this._yamlMode || selected === -1;
return html`
<ha-card>
<div class="card-content">
<div class="card-menu">
${this.index !== 0
? html`
<paper-icon-button
icon="hass:arrow-up"
@click=${this._moveUp}
></paper-icon-button>
`
: ""}
${this.index !== this.totalActions - 1
? html`
<paper-icon-button
icon="hass:arrow-down"
@click=${this._moveDown}
></paper-icon-button>
`
: ""}
<paper-menu-button
no-animations
horizontal-align="right"
horizontal-offset="-5"
vertical-offset="-5"
close-on-activate
>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item
@click=${this._switchYamlMode}
.disabled=${selected === -1}
>
${yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</paper-item>
<paper-item disabled>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</paper-item>
<paper-item @click=${this._onDelete}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
</div>
${yamlMode
? html`
<div style="margin-right: 24px;">
${selected === -1
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.actions.unsupported_action",
"action",
type
)}
`
: ""}
<ha-yaml-editor
.value=${this.action}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
</div>
`
: html`
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type_select"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${selected}
@iron-select=${this._typeChanged}
>
${OPTIONS.map(
(opt) => html`
<paper-item .action=${opt}>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${opt}.label`
)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
<div>
${dynamicElement(`ha-automation-action-${type}`, {
hass: this.hass,
action: this.action,
})}
</div>
`}
</div>
</ha-card>
`;
}
private _moveUp() {
fireEvent(this, "move-action", { direction: "up" });
}
private _moveDown() {
fireEvent(this, "move-action", { direction: "down" });
}
private _onDelete() {
if (
confirm(
this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm"
)
)
) {
fireEvent(this, "value-changed", { value: null });
}
}
private _typeChanged(ev: CustomEvent) {
const type = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.action;
if (!type) {
return;
}
if (type !== getType(this.action)) {
const elClass = customElements.get(`ha-automation-action-${type}`);
fireEvent(this, "value-changed", {
value: {
...elClass.defaultConfig,
},
});
}
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", { value: ev.detail.value });
}
private _switchYamlMode() {
this._yamlMode = !this._yamlMode;
}
static get styles(): CSSResult {
return css`
.card-menu {
position: absolute;
top: 0;
right: 0;
z-index: 3;
color: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
.card-menu paper-item {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-row": HaAutomationActionRow;
}
}

View File

@@ -0,0 +1,98 @@
import "@material/mwc-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
import { Action } from "../../../../data/script";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-action-row";
@customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement {
@property() public hass!: HomeAssistant;
@property() public actions!: Action[];
protected render() {
return html`
${this.actions.map(
(action, idx) => html`
<ha-automation-action-row
.index=${idx}
.totalActions=${this.actions.length}
.action=${action}
@move-action=${this._move}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action-row>
`
)}
<ha-card>
<div class="card-actions add-card">
<mwc-button @click=${this._addAction}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.add"
)}
</mwc-button>
</div>
</ha-card>
`;
}
private _addAction() {
const actions = this.actions.concat({
service: "",
});
fireEvent(this, "value-changed", { value: actions });
}
private _move(ev: CustomEvent) {
const index = (ev.target as any).index;
const newIndex = ev.detail.direction === "up" ? index - 1 : index + 1;
const actions = this.actions.concat();
const action = actions.splice(index, 1)[0];
actions.splice(newIndex, 0, action);
fireEvent(this, "value-changed", { value: actions });
}
private _actionChanged(ev: CustomEvent) {
ev.stopPropagation();
const actions = [...this.actions];
const newValue = ev.detail.value;
const index = (ev.target as any).index;
if (newValue === null) {
actions.splice(index, 1);
} else {
actions[index] = newValue;
}
fireEvent(this, "value-changed", { value: actions });
}
static get styles(): CSSResult {
return css`
ha-automation-action-row,
ha-card {
display: block;
margin-top: 16px;
}
.add-card mwc-button {
display: block;
text-align: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action": HaAutomationAction;
}
}

View File

@@ -0,0 +1,41 @@
import "../../condition/ha-automation-condition-editor";
import { LitElement, property, customElement, html } from "lit-element";
import { ActionElement } from "../ha-automation-action-row";
import { HomeAssistant } from "../../../../../types";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { Condition } from "../../../../../data/automation";
@customElement("ha-automation-action-condition")
export class HaConditionAction extends LitElement implements ActionElement {
@property() public hass!: HomeAssistant;
@property() public action!: Condition;
public static get defaultConfig() {
return { condition: "state" };
}
public render() {
return html`
<ha-automation-condition-editor
.condition=${this.action}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
></ha-automation-condition-editor>
`;
}
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: ev.detail.value,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-condition": HaConditionAction;
}
}

View File

@@ -0,0 +1,43 @@
import "@polymer/paper-input/paper-input";
import "../../../../../components/ha-service-picker";
import "../../../../../components/entity/ha-entity-picker";
import { LitElement, property, customElement, html } from "lit-element";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
import { HomeAssistant } from "../../../../../types";
import { DelayAction } from "../../../../../data/script";
@customElement("ha-automation-action-delay")
export class HaDelayAction extends LitElement implements ActionElement {
@property() public hass!: HomeAssistant;
@property() public action!: DelayAction;
public static get defaultConfig() {
return { delay: "" };
}
public render() {
const { delay } = this.action;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.delay.delay"
)}
name="delay"
.value=${delay}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-delay": HaDelayAction;
}
}

View File

@@ -0,0 +1,129 @@
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/device/ha-device-action-picker";
import "../../../../../components/ha-form/ha-form";
import {
fetchDeviceActionCapabilities,
deviceAutomationsEqual,
DeviceAction,
} from "../../../../../data/device_automation";
import { LitElement, customElement, property, html } from "lit-element";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../../types";
@customElement("ha-automation-action-device_id")
export class HaDeviceAction extends LitElement {
@property() public hass!: HomeAssistant;
@property() public action!: DeviceAction;
@property() private _deviceId?: string;
@property() private _capabilities?;
private _origAction?: DeviceAction;
public static get defaultConfig() {
return {
device_id: "",
domain: "",
entity_id: "",
};
}
protected render() {
const deviceId = this._deviceId || this.action.device_id;
const extraFieldsData =
this._capabilities && this._capabilities.extra_fields
? this._capabilities.extra_fields.map((item) => {
return { [item.name]: this.action[item.name] };
})
: undefined;
return html`
<ha-device-picker
.value=${deviceId}
@value-changed=${this._devicePicked}
.hass=${this.hass}
label="Device"
></ha-device-picker>
<ha-device-action-picker
.value=${this.action}
.deviceId=${deviceId}
@value-changed=${this._deviceActionPicked}
.hass=${this.hass}
label="Action"
></ha-device-action-picker>
${extraFieldsData
? html`
<ha-form
.data=${Object.assign({}, ...extraFieldsData)}
.schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
)}
@value-changed=${this._extraFieldsChanged}
></ha-form>
`
: ""}
`;
}
protected firstUpdated() {
if (!this._capabilities) {
this._getCapabilities();
}
if (this.action) {
this._origAction = this.action;
}
}
protected updated(changedPros) {
const prevAction = changedPros.get("action");
if (prevAction && !deviceAutomationsEqual(prevAction, this.action)) {
this._getCapabilities();
}
}
private async _getCapabilities() {
const action = this.action;
this._capabilities = action.domain
? await fetchDeviceActionCapabilities(this.hass, action)
: null;
}
private _devicePicked(ev) {
ev.stopPropagation();
this._deviceId = ev.target.value;
}
private _deviceActionPicked(ev) {
ev.stopPropagation();
let action = ev.detail.value;
if (this._origAction && deviceAutomationsEqual(this._origAction, action)) {
action = this._origAction;
}
fireEvent(this, "value-changed", { value: action });
}
private _extraFieldsChanged(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.action,
...ev.detail.value,
},
});
}
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.actions.type.device.extra_fields.${schema.name}`
) || schema.name;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-device_id": HaDeviceAction;
}
}

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