Compare commits

...

124 Commits

Author SHA1 Message Date
Paulus Schoutsen
67c032c85a Merge pull request #2429 from home-assistant/dev
2019109.0
2019-01-09 14:39:11 -08:00
Paulus Schoutsen
417ffde3e8 Update translations 2019-01-09 14:37:32 -08:00
Paulus Schoutsen
f3064f0071 Bumped version to 2019109.0 2019-01-09 14:36:55 -08:00
Bram Kragten
e9d912cc87 Remove styleMap for base-unit (#2428) 2019-01-09 14:04:49 -08:00
yosilevy
2517e5ba60 Proper RTL support in weather forecast card (#2424)
* Proper RTL support in weather forecast card

* States panel force left to right since RTL is not usable

* Updated class names and switched div to span
2019-01-09 21:34:24 +01:00
Bram Kragten
64b405dd4d Set min height to thermostat card (#2416)
* Set min height

* Set min height on #root

* Add min-height on firstUpdated and remove after roundSlider is loaded

* Move back to #thermostat
2019-01-08 11:27:01 -08:00
Bram Kragten
ddb050d1fd Change title to name (#2425) 2019-01-08 08:18:44 -06:00
hulkhaugen
3f6a8cac80 Removed excessive bracket in css (#2411)
* Removed excessive bracket in css code

* lint
2019-01-07 10:40:12 +01:00
Bram Kragten
ad113367e6 🛠️ Small fixes to sensor editor (#2415)
* Small fixes to sensor editor

* any -> string | number
2019-01-06 20:06:59 -06:00
David F. Mulcahey
f4f08ab0d1 Config panel for ZHA (#2389)
* zha config panel

* implement issue cluster command

* update layout

* read zigbee attribute service

* set attribute and manufacturer code override

* cleanup

* adjust style and documentation wording

* html cleanup

* ha-call-ws-api-button

* use call-ws-api-button

* fix deprecated syntax - travis error

* emulate new z-wave node info functionality

* start converting to lit

* fix style includes

* fix help toggle

* remove old panel

* cleanup

* cleanup

* convert to lit / ts

* import styles

* types - review comment
2019-01-04 19:11:08 -06:00
John Arild Berentsen
c2a57099d3 Allow for removal of broadcast node in UI (#2390)
* allow for removal of broadcast

* prettier

* prettier
2019-01-03 23:31:30 +01:00
Emil Stjerneman
adf0c6d891 Lovelace alarm panel respects the code_format attribute on the alarm entity (#2379) 2019-01-03 23:09:07 +01:00
Milan V
38a2627227 Fix climate control rounding error (#2375)
* Fix climate rounding error

* Update ha-climate-control.js
2019-01-03 23:07:37 +01:00
Ian Richardson
5a90edc893 thermostat card: follow same step logic as more-info (#2369)
* Follow same step logic as more-info

* Address review comments

* Address review comments

* Address review comments and cleanup
2019-01-03 22:53:55 +01:00
Ian Richardson
88473581c2 UI Editor for entity-button card (#2393) 2019-01-03 22:53:10 +01:00
Bram Kragten
88d23eb9dd Check config on save (#2346)
* Check config on save

Untested...

* views is list of objects, resources not required

* Lint

* Only warn on "# " to prevent warnings on colors

Could miss some comments...

* Improve # check

Warn on # at the start of a line or when followed by white space

* Faster

* Multiline

* Check if # is added

* Also handle paste and just show the warning on every #
2019-01-03 22:46:57 +01:00
Andrey
25c788871f Allow empty name (#2388) 2019-01-03 22:46:14 +01:00
Xus Badia
f272801253 Update favicon-apple-180x180.png (#2385) 2018-12-28 18:22:40 +01:00
Ian Richardson
2e750dc1e2 UI Editor for map card (#2287)
*  UI Editor for `map` card

* Address review comments

* Revert change properly

* Address review comments

* Note to change to interface after LL conversion

* Remove config options if empty

Should apply to other editors

* entitites is required

* cleanup

* cleanup

* Fix for number values

* Name chunk

* Remove zoom default
2018-12-21 13:53:26 +01:00
Paulus Schoutsen
3c5fb6d1ad Update translations 2018-12-19 14:43:12 +01:00
Paulus Schoutsen
32cd683b8a Bumped version to 20181219.0 2018-12-19 14:42:39 +01:00
Paulus Schoutsen
6c029b39e0 Merge branch 'master' into dev 2018-12-19 14:03:44 +01:00
Paulus Schoutsen
7efad04e42 Merge pull request #2367 from home-assistant/20181211-2
20181211.2
2018-12-19 14:02:48 +01:00
Paulus Schoutsen
b6f7781a87 Bumped version to 20181211.2 2018-12-19 13:59:16 +01:00
Paulus Schoutsen
16a147f389 Fix incorrect state display being cached (#2356)
* Fix incorrect state display being cached

* Remove test for cache
2018-12-19 13:59:10 +01:00
Zack Arnett
79b71ed753 Update UI and Add Move Card to a Dialogue (#2282)
* Update UI

* REmove useless import

* Add Notification if saved is done

* Move Register Dialog

* Updates from suggestions

* Updates box shadow on card options

* Update color and viewselection

* Add pointer

* Update Cursor

* Update check next to save

* Comment Updates

* Text area
2018-12-19 13:08:17 +01:00
Paulus Schoutsen
49fa74cc07 Fix incorrect state display being cached (#2356)
* Fix incorrect state display being cached

* Remove test for cache
2018-12-19 13:05:39 +01:00
Ian Richardson
0a2eaec884 Cleanup of picture card editor (#2364)
* Cleanup of `picture` card editor

* Fix `action-editor`
2018-12-18 20:40:51 +01:00
Ian Richardson
4c5d3138c1 UI Editor for picture card (#2240)
* UI Editor for `picture` card

This is a WIP.
* How should I handle service data? It's kind of freeform and I don't really have a good idea on what I should do.
* in action-editor I have two issues for `_navigation_path` and `_service` have TS errors saying the property doesn't exist on `ToggleActionConfig`. Not sure why that is the only type it is looking at. Should I be checking the type somewhere?

* Remove `id`

* Cleanup. Service-data still WIP

* Could use some help on service_data

* Perhaps a better/more structured method?

* Revert "Perhaps a better/more structured method?"

This reverts commit 1e1a1e44c16a18c5ffc380347cffd01e7fad52f9.

* Just playing around

* MVP doesn't include service data

* Address review comments

* Address review comments

* Name chunk and remove when unused

* Remove `more-info` action option

* Address review comments
2018-12-18 17:25:56 +01:00
Paulus Schoutsen
5e1cd389b3 Name all chunks (#2363) 2018-12-18 17:14:33 +01:00
Ian Richardson
7ced08a899 UI Editor for weather-forecast card (#2285)
*  UI Editor for `weather-forecast` card

* Missed name

* Fix typo

* Address review comments

* Remove unused optional configs with no defaults

* cleanup

* Name chunk
2018-12-18 14:26:34 +01:00
Malte Franken
603cf7ba0f two new geo location map demo cards (#2349) 2018-12-18 11:29:12 +01:00
Ian Richardson
c47ba65c3b 🧹 remove unused configs when empty (#2347)
* Remove unused configs when empty

* Handle unused numbers & fix glance entities

* Sneak in the chunk names
2018-12-18 11:15:32 +01:00
Ian Richardson
849ed80e78 🚫 render error-entity-row when state not available (#2352) 2018-12-18 11:14:55 +01:00
quthla
b78c48ecec Fix android external auth (#2319)
* Fix android external auth

* Update external_auth.ts

* Update external_auth.ts

* Update external_auth.ts
2018-12-17 20:37:10 +01:00
Paulus Schoutsen
8d2da9c5a6 Merge branch 'master' into dev 2018-12-17 10:51:19 +01:00
Ian Richardson
9664e8258c UI Editor for plant-status card (#2286)
*  UI Editor for `plant-status` card

* Address review comments

* Require entity

* Cleanup

* Cleanup

* Address review comments

* Update hui-plant-status-card-editor.ts
2018-12-17 10:45:23 +01:00
Paulus Schoutsen
5f5bf17df0 Merge pull request #2345 from home-assistant/20181211-1
20181211 1
2018-12-17 10:20:35 +01:00
Paulus Schoutsen
ca7674cd15 Bumped version to 20181211.1 2018-12-17 10:17:32 +01:00
Paulus Schoutsen
3f5f5bb1ee Fix service button element (#2343) 2018-12-17 10:17:18 +01:00
Paulus Schoutsen
e7ee9c7054 Fix undefined on plant/weather card (#2339) 2018-12-17 10:17:18 +01:00
Ian Richardson
4f6ecf5c21 🔨 Fix for element positioning (#2335)
* Fix for element positioning

* Address comments
2018-12-17 10:17:17 +01:00
Zack Arnett
87eac4cdee remove Animation for thermostat and light (#2303)
* Update Animation

* Update light
2018-12-17 10:17:16 +01:00
Paulus Schoutsen
d267196bff Call super updated (#2293) 2018-12-17 10:16:50 +01:00
Paulus Schoutsen
f683337cbe Fix opening edit dialog twice when closed by clicking on overlay (#2290) 2018-12-17 10:13:19 +01:00
Paulus Schoutsen
1a6226270f Fix setting aspect ratio in percentage (#2289)
* Fix setting aspect ratio in percentage

* Use endsWith

* Fix invalid test
2018-12-17 10:13:19 +01:00
Paulus Schoutsen
64714c64c7 Fix notifications drawer (#2344) 2018-12-17 10:08:23 +01:00
Ian Richardson
b7c34c483a 🔨 Fix for element positioning (#2335)
* Fix for element positioning

* Address comments
2018-12-17 10:07:59 +01:00
Paulus Schoutsen
e5bf842801 Fix service button element (#2343) 2018-12-17 09:36:55 +01:00
Ian Richardson
d1a56d6acc UI Editor for media-control card (#2336)
*  UI Editor for `media-control` card

* Address review comments

* Note on LL interface
2018-12-17 09:31:58 +01:00
Malte Franken
cac7f8d1ab Extended map to support geo location entities (#2337)
* initial version of geo location map

* configuring entities not required but source is

* extending existing map instead of adding a new one

* renamed source to geo_location_source; clearer handling of geo location entities vs defined entities

* geo location sources must now be an array

* code cleanup
2018-12-17 09:04:58 +01:00
Ian Richardson
9d2b37c9f2 Remove unnecessary editor types (#2342) 2018-12-17 07:50:27 +01:00
Paulus Schoutsen
e20a02c52c Fix undefined on plant/weather card (#2339) 2018-12-16 13:52:13 -05:00
Zack Arnett
c46d04eaa6 Alarm Card Conversion (#2259)
* Alarm Card COnversion

* Map buttons

* Reducing margin under alarm code

* Update src/panels/lovelace/cards/hui-alarm-panel-card.ts

Co-Authored-By: zsarnett <arnett.zackary@gmail.com>

* Comment Updates

* Review updates

* Reorder css

* Update actions

* TSlint

* TS LINT

* Idk whats going on here

* Fix import
2018-12-14 11:16:43 +01:00
Zack Arnett
2ec8b97378 remove Animation for thermostat and light (#2303)
* Update Animation

* Update light
2018-12-13 21:45:21 -05:00
Paulus Schoutsen
b3b9ca9c3f Fix setting aspect ratio in percentage (#2289)
* Fix setting aspect ratio in percentage

* Use endsWith

* Fix invalid test
2018-12-13 23:36:01 +01:00
Paulus Schoutsen
71ed83ef07 Fix opening edit dialog twice when closed by clicking on overlay (#2290) 2018-12-13 21:46:57 +01:00
Paulus Schoutsen
47635055d0 Clear view cache edit mode (#2291)
* Clear view cache when canceling edit mode

* Fix enabling edit mode on not first view
2018-12-13 21:46:50 +01:00
Paulus Schoutsen
0dfca2f33b Call super updated (#2293) 2018-12-13 09:26:23 -05:00
Ian Richardson
18de427145 🧹 domain-filter as attribute as it does not change (#2271)
* 🔨 `domain-filter` as attribute as it does not change

* Address review comments
2018-12-13 09:40:57 +01:00
Bram Kragten
118f28285e 📷 Add camera card on generate (#2279) 2018-12-12 08:00:00 -06:00
Paulus Schoutsen
6a9cfbfa1c TS create element functions (#2276)
* TS create element functions

* Name chunk
2018-12-12 14:21:34 +01:00
Bram Kragten
8c61624a9c Change edit mode + add edit lovelace (#2277)
* Change edit mode + add edit lovelace

* Comments

* Fix (yes it was used :-)

* Raw edit header changed
2018-12-12 13:59:19 +01:00
Paulus Schoutsen
d277571735 Generalize errors (#2275) 2018-12-12 09:47:32 +01:00
Paulus Schoutsen
a6f3684846 Fix gauge (#2274) 2018-12-12 09:29:46 +01:00
Paulus Schoutsen
edef4ba2f5 Speed things up (#2270)
* Speed things up

* Fixes
2018-12-12 08:51:59 +01:00
Matthias Dötsch
7cd6619a79 Added some missing translate tags for cards (#2188)
* Added some missing translate tags for cards

more-info of:
 - sun
 - script
 - updater

* Added some missing translate tags for cards

* Added some missing translate tags for cards
2018-12-12 08:27:56 +01:00
Ian Richardson
2059e36dd6 🔨 Update value attributes to properties (#2269) 2018-12-12 08:12:57 +01:00
Ian Richardson
4a455e9147 🔧 Organize editor imports (#2272) 2018-12-12 08:09:23 +01:00
Ian Richardson
fe0b131480 UI Editor for iframe card (#2230)
* UI Editor for `iframe` card

* Remove `id`

* Address review comments

* Cleaned up some inconsistencies
2018-12-11 17:05:01 -06:00
Paulus Schoutsen
b1b78c2bb7 Fix stack trace in badge 2018-12-11 21:43:43 +01:00
Paulus Schoutsen
99395360c7 Convert HA-STATE-LABEL-BADGE to lit (#2268) 2018-12-11 21:41:38 +01:00
Ian Richardson
bd46e3b8e0 UI Editor for sensor card (#2267) 2018-12-11 21:39:18 +01:00
Paulus Schoutsen
80dd15306e Convert ha-label-badge to lit (#2266)
* Convert ha-label-badge to lit

* Add class to TypeScript map
2018-12-11 21:23:19 +01:00
Ian Richardson
88f0ebf75d UI editor for shopping-list card (#2227)
* Add UI editor for `shopping-list` card

* Cleanup

* Export config

* Remove `id`
2018-12-11 21:23:10 +01:00
Ian Richardson
8679f10f10 UI Editor for light card (#2232)
* UI Editor for `light` card

* Remove `id`
2018-12-11 21:22:21 +01:00
Ian Richardson
db4c1e45f5 UI Editor for gauge card (#2229)
* UI editor for `gauge` card

Need to develop a severity input method still

* Config works well but no preview showing

* Add `sensor` domain filter

* Remove `id`
2018-12-11 21:22:08 +01:00
Paulus Schoutsen
65cf2feb7a UI Editor for markdown card (#2231)
* UI Editor for `markdown` card

* Remove `id`
2018-12-11 21:21:38 +01:00
Ian Richardson
97da26eba7 UI Editor for alarm-panel card (#2257)
* UI Editor for `alarm-panel` card

* Adding of states

Can't get the last available state to be recognized as being a selection change

* Ability to remove states

* Clean up

* Clean up

* Remove id
2018-12-11 21:15:29 +01:00
Ian Richardson
8e7d7c5188 UI Editor for thermostat card (#2258) 2018-12-11 21:15:18 +01:00
Paulus Schoutsen
767307ef47 Convert hui-view to Lit (#2265)
* Convert hui-view to Lit

* Add super call to updated

* Update src/panels/lovelace/hui-view.ts

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

* Apply suggestions from code review

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

* Address comments"

* Fix things
2018-12-11 19:47:19 +01:00
Paulus Schoutsen
ccc6262026 Convert HUI-ROOT to Lit Element (#2264)
* Convert HUI-ROOT to Lit Element

* Update src/panels/lovelace/hui-root.ts

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

* Update src/panels/lovelace/hui-root.ts

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

* Update src/panels/lovelace/hui-root.ts

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

* Update src/panels/lovelace/hui-root.ts

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

* Update src/panels/lovelace/hui-root.ts

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

* Update src/panels/lovelace/hui-root.ts

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

* Update src/panels/lovelace/hui-root.ts

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

* Update src/panels/lovelace/hui-root.ts

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

* Update src/panels/lovelace/hui-root.ts

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

* Update src/panels/lovelace/hui-root.ts

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

* Apply suggestions from code review

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

* Address comments
2018-12-11 19:39:40 +01:00
Bram Kragten
2cdb542112 Add move card to view (#2262)
* Add move card to view

* Fix style

* Fix style and tests

* last style change

* update tests
2018-12-11 19:38:57 +01:00
Paulus Schoutsen
4e232e58ce Address comments 2018-12-11 19:26:26 +01:00
Ian Richardson
27bb175624 Apply suggestions from code review
Co-Authored-By: balloob <paulus@home-assistant.io>
2018-12-11 19:05:01 +01:00
Ian Richardson
5a5a7dad1e Update src/panels/lovelace/hui-root.ts
Co-Authored-By: balloob <paulus@home-assistant.io>
2018-12-11 19:03:58 +01:00
Ian Richardson
2d1cf421ef Update src/panels/lovelace/hui-root.ts
Co-Authored-By: balloob <paulus@home-assistant.io>
2018-12-11 19:03:48 +01:00
Ian Richardson
8be25f2020 Update src/panels/lovelace/hui-root.ts
Co-Authored-By: balloob <paulus@home-assistant.io>
2018-12-11 19:03:33 +01:00
Ian Richardson
0a8f853a8e Update src/panels/lovelace/hui-root.ts
Co-Authored-By: balloob <paulus@home-assistant.io>
2018-12-11 19:03:01 +01:00
Ian Richardson
a46f5e3d4e Update src/panels/lovelace/hui-root.ts
Co-Authored-By: balloob <paulus@home-assistant.io>
2018-12-11 19:02:44 +01:00
Ian Richardson
5de36f9579 Update src/panels/lovelace/hui-root.ts
Co-Authored-By: balloob <paulus@home-assistant.io>
2018-12-11 19:02:32 +01:00
Ian Richardson
9b5e79f42a Update src/panels/lovelace/hui-root.ts
Co-Authored-By: balloob <paulus@home-assistant.io>
2018-12-11 19:02:18 +01:00
Ian Richardson
a824599a37 Update src/panels/lovelace/hui-root.ts
Co-Authored-By: balloob <paulus@home-assistant.io>
2018-12-11 19:02:07 +01:00
Ian Richardson
884b24da0e Update src/panels/lovelace/hui-root.ts
Co-Authored-By: balloob <paulus@home-assistant.io>
2018-12-11 19:01:32 +01:00
Ian Richardson
76325a384c Update src/panels/lovelace/hui-root.ts
Co-Authored-By: balloob <paulus@home-assistant.io>
2018-12-11 19:00:57 +01:00
Paulus Schoutsen
e2218f1e6e Extract component for editing out of hui-view (#2263) 2018-12-11 17:17:59 +01:00
Paulus Schoutsen
758b686684 Convert HUI-ROOT to Lit Element 2018-12-11 17:06:05 +01:00
Paulus Schoutsen
3a50d47dd2 Merge pull request #2261 from home-assistant/dev
20181211.0
2018-12-11 10:29:13 +01:00
Paulus Schoutsen
b4d4591273 Bumped version to 20181211.0 2018-12-11 10:28:16 +01:00
Paulus Schoutsen
432fdd628c Update translations 2018-12-11 10:28:12 +01:00
Paulus Schoutsen
bc23dd37be Handle webhook component not loaded (#2255)
* Handle webhook component not loaded

* Fix text
2018-12-11 10:25:16 +01:00
Ian Richardson
0319fd23c5 Remove id 2018-12-10 23:13:26 -06:00
Ian Richardson
d7e5993501 UI Editor for markdown card 2018-12-10 23:12:22 -06:00
Paulus Schoutsen
46a9b90ed0 Add webhook trigger to automation editor (#2252) 2018-12-10 19:45:48 +01:00
Bram Kragten
b0c68e58c5 Small fixes (#2248)
* Small fixes

* remove returning on undefined in statename

* 16px -> 4px

* Styling
2018-12-10 19:44:34 +01:00
Paulus Schoutsen
7063ced7fd Merge pull request #2247 from home-assistant/dev
20181210.1
2018-12-10 12:49:26 +01:00
Paulus Schoutsen
7d2444868d Bumped version to 20181210.1 2018-12-10 12:48:37 +01:00
Paulus Schoutsen
0899d42967 Add raw config editor for storage mode (#2246)
* Add raw config editor for storage mode

* Lint

* Allow inserting spaces by pressing tab
2018-12-10 12:48:05 +01:00
Bram Kragten
d57bcc2701 Disable edit in yaml mode (#2244) 2018-12-10 11:40:34 +01:00
Paulus Schoutsen
d9d92c8766 Simplify Lovelace mode (#2243) 2018-12-10 11:38:18 +01:00
Paulus Schoutsen
7a16ed5400 Merge pull request #2242 from home-assistant/dev
20181210.0
2018-12-10 09:51:55 +01:00
Paulus Schoutsen
07e35ff81a Update translations 2018-12-10 09:50:25 +01:00
Paulus Schoutsen
0398944cab Bumped version to 20181210.0 2018-12-10 09:49:42 +01:00
Paulus Schoutsen
a33ff7479a Allow moving cards (#2241) 2018-12-10 09:48:59 +01:00
Paulus Schoutsen
f9182e5453 Revert lovelace as default (#2237)
* Revert lovelace as default

* Better text
2018-12-10 08:57:51 +01:00
Bram Kragten
4f0a965573 Changes for new storage (#2228)
* Changes for new storage

* Fix lint

* Use indexes for editing

* Use lovelace object

* Use lovelace object

* Lit conversion panel lovelace

* Lovelace obj

* Fix edit cards

* Fix some bugs

* Fix last bugs

* Lint

* Don't drop view content

* Move file

* Add skip button to card picker

* Correctly set lovelace mode
2018-12-10 08:57:29 +01:00
Paulus Schoutsen
2a23487163 Fix long press in FF (#2226)
* Fix long press in FF

* Better fix
2018-12-09 10:09:34 +01:00
Ian Richardson
6b9ba7367d Remove alarm-panel UI editor (#2235)
Remove the editor that I inadvertidently committed to dev so that we can give it a proper review before merging.
2018-12-09 10:09:10 +01:00
Paulus Schoutsen
a17ae5546f Lint 2018-12-08 10:54:00 +01:00
Ian Richardson
47de0e9156 UI Editor for alarm-panel card
Still need to get states sorted out
2018-12-07 22:54:18 -06:00
Bram Kragten
baeda622de Save columns as string (#2214)
* save columns as string

* set type to number to convert to number before save
2018-12-07 14:14:29 +01:00
Bram Kragten
48220b67ed Fix edit view and layout (#2213)
* Fix edit view and layout

* Colors
2018-12-07 13:20:24 +01:00
Ian Richardson
9ba232249b grammer tweaks (#2209) 2018-12-07 11:44:55 +01:00
Bram Kragten
b0580e70d2 Fix horizontal stack (#2210) 2018-12-07 11:43:49 +01:00
187 changed files with 8309 additions and 2990 deletions

View File

@@ -7,7 +7,7 @@ import { demoConfig } from "../data/demo_config";
import { demoServices } from "../data/demo_services";
import demoResources from "../data/demo_resources";
import demoStates from "../data/demo_states";
import createCardElement from "../../../src/panels/lovelace/common/create-card-element";
import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element";
class DemoCard extends PolymerElement {
static get template() {
@@ -78,6 +78,10 @@ class DemoCard extends PolymerElement {
hass.resources = demoResources;
hass.language = "en";
hass.states = demoStates;
hass.themes = {
default_theme: "default",
themes: {},
};
el.hass = hass;
}

View File

@@ -29,6 +29,31 @@ const ENTITIES = [
friendly_name: "Home",
icon: "mdi:home",
}),
getEntity("zone", "bushfire", "zoning", {
latitude: -33.8611,
longitude: 151.203,
radius: 35000,
friendly_name: "Bushfire Zone",
icon: "mdi:home",
}),
getEntity("geo_location", "nelsons_creek", "15", {
source: "bushfire_demo",
latitude: -34.07792,
longitude: 151.03219,
friendly_name: "Nelsons Creek",
}),
getEntity("geo_location", "forest_rd_nowra_hill", "8", {
source: "bushfire_demo",
latitude: -33.69452,
longitude: 151.19577,
friendly_name: "Forest Rd, Nowra Hill",
}),
getEntity("geo_location", "stoney_ridge_rd_kremnos", "20", {
source: "bushfire_demo",
latitude: -33.66584,
longitude: 150.97209,
friendly_name: "Stoney Ridge Rd, Kremnos",
}),
];
const CONFIGS = [
@@ -116,6 +141,24 @@ const CONFIGS = [
- light.bed_light
`,
},
{
heading: "Geo Location Entities",
config: `
- type: map
geo_location_sources:
- bushfire_demo
`,
},
{
heading: "Geo Location Entities with Home Zone",
config: `
- type: map
geo_location_sources:
- bushfire_demo
entities:
- zone.bushfire
`,
},
];
class DemoMap extends PolymerElement {

View File

@@ -76,6 +76,55 @@ const CONFIGS = [
left: 35%
`,
},
{
heading: "Card with header",
config: `
- type: picture-elements
image: /images/floorplan.png
title: My House
elements:
- type: service-button
title: Lights Off
style:
top: 97%
left: 90%
padding: 0px
service: light.turn_off
service_data:
entity_id: group.all_lights
- type: icon
icon: mdi:cctv
entity: camera.demo_camera
style:
top: 12%
left: 6%
transform: rotate(-60deg) scaleX(-1)
--iron-icon-height: 30px
--iron-icon-width: 30px
--iron-icon-stroke-color: black
--iron-icon-fill-color: rgba(50, 50, 50, .75)
- type: image
entity: light.bed_light
tap_action:
action: toggle
image: /images/light_bulb_off.png
state_image:
'on': /images/light_bulb_on.png
state_filter:
'on': brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)
'off': brightness(80%) saturate(0.8)
style:
top: 35%
left: 65%
width: 7%
padding: 50px 50px 100px 50px
- type: state-icon
entity: binary_sensor.movement_backyard
style:
top: 8%
left: 35%
`,
},
];
class DemoPicElements extends PolymerElement {

View File

@@ -9,6 +9,9 @@ const ENTITIES = [
getEntity("light", "kitchen_lights", "on", {
friendly_name: "Kitchen Lights",
}),
getEntity("light", "bed_light", "on", {
friendly_name: "Bed Lights",
}),
getEntity("device_tracker", "demo_paulus", "work", {
source_type: "gps",
latitude: 32.877105,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

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

View File

@@ -119,7 +119,7 @@ class HaPlantCard extends EventsMixin(PolymerElement) {
}
computeTitle(stateObj) {
return this.config.name || computeStateName(stateObj);
return (this.config && this.config.name) || computeStateName(stateObj);
}
computeAttributes(data) {

View File

@@ -8,6 +8,7 @@ import "../components/ha-icon";
import EventsMixin from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
import { computeRTL } from "../common/util/compute_rtl";
/*
* @appliesMixin LocalizeMixin
@@ -30,15 +31,20 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
.header {
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(--paper-font-headline_-_-webkit-font-smoothing);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
text-rendering: var(--paper-font-common-expensive-kerning_-_text-rendering);
text-rendering: var(
--paper-font-common-expensive-kerning_-_text-rendering
);
opacity: var(--dark-primary-opacity);
padding: 24px 16px 16px;
display: flex;
align-items: baseline;
}
.name {
@@ -47,6 +53,11 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
color: var(--secondary-text-color);
}
:host([rtl]) .name {
margin-left: 0px;
margin-right: 16px;
}
.now {
display: flex;
justify-content: space-between;
@@ -60,18 +71,31 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
margin-right: 32px;
}
:host([rtl]) .main {
margin-right: 0px;
}
.main ha-icon {
--iron-icon-height: 72px;
--iron-icon-width: 72px;
margin-right: 8px;
}
:host([rtl]) .main ha-icon {
margin-right: 0px;
}
.main .temp {
font-size: 52px;
line-height: 1em;
position: relative;
}
:host([rtl]) .main .temp {
direction: ltr;
margin-right: 28px;
}
.main .temp span {
font-size: 24px;
line-height: 1em;
@@ -79,6 +103,14 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
top: 4px;
}
.measurand {
display: inline-block;
}
:host([rtl]) .measurand {
direction: ltr;
}
.forecast {
margin-top: 16px;
display: flex;
@@ -95,13 +127,17 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
text-align: center;
}
:host([rtl]) .forecast .temp {
direction: ltr;
}
.weekday {
font-weight: bold;
}
.attributes,
.templow,
.precipitation { {
.precipitation {
color: var(--secondary-text-color);
}
</style>
@@ -129,7 +165,9 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
>
<div>
[[localize('ui.card.weather.attributes.air_pressure')]]:
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
<span class="measurand">
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
</span>
</div>
</template>
<template
@@ -138,7 +176,9 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
>
<div>
[[localize('ui.card.weather.attributes.humidity')]]:
[[stateObj.attributes.humidity]] %
<span class="measurand"
>[[stateObj.attributes.humidity]] %</span
>
</div>
</template>
<template
@@ -147,8 +187,10 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
>
<div>
[[localize('ui.card.weather.attributes.wind_speed')]]:
[[getWind(stateObj.attributes.wind_speed,
stateObj.attributes.wind_bearing, localize)]]
<span class="measurand">
[[getWindSpeed(stateObj.attributes.wind_speed)]]
</span>
[[getWindBearing(stateObj.attributes.wind_bearing, localize)]]
</div>
</template>
</div>
@@ -195,11 +237,17 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get properties() {
return {
hass: Object,
config: Object,
stateObj: Object,
forecast: {
type: Array,
computed: "computeForecast(stateObj.attributes.forecast)",
},
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
};
}
@@ -274,7 +322,7 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
computeName(stateObj) {
return this.config.name || computeStateName(stateObj);
return (this.config && this.config.name) || computeStateName(stateObj);
}
showWeatherIcon(condition) {
@@ -293,14 +341,18 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
return degree;
}
getWind(speed, bearing, localize) {
getWindSpeed(speed) {
return `${speed} ${this.getUnit("length")}/h`;
}
getWindBearing(bearing, localize) {
if (bearing != null) {
const cardinalDirection = this.windBearingToText(bearing);
return `${speed} ${this.getUnit("length")}/h (${localize(
return `(${localize(
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
) || cardinalDirection})`;
}
return `${speed} ${this.getUnit("length")}/h`;
return ``;
}
_showValue(item) {
@@ -322,5 +374,9 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
{ hour: "numeric" }
);
}
_computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("ha-weather-card", HaWeatherCard);

View File

@@ -18,8 +18,8 @@ interface RefreshTokenResponse {
declare global {
interface Window {
externalApp?: {
getExternalAuth(payload: BasePayload);
revokeExternalAuth(payload: BasePayload);
getExternalAuth(payload: string);
revokeExternalAuth(payload: string);
};
webkit?: {
messageHandlers: {
@@ -67,7 +67,7 @@ export default class ExternalAuth extends Auth {
const callbackPayload = { callback: CALLBACK_SET_TOKEN };
if (window.externalApp) {
window.externalApp.getExternalAuth(callbackPayload);
window.externalApp.getExternalAuth(JSON.stringify(callbackPayload));
} else {
window.webkit!.messageHandlers.getExternalAuth.postMessage(
callbackPayload
@@ -92,7 +92,7 @@ export default class ExternalAuth extends Auth {
const callbackPayload = { callback: CALLBACK_REVOKE_TOKEN };
if (window.externalApp) {
window.externalApp.revokeExternalAuth(callbackPayload);
window.externalApp.revokeExternalAuth(JSON.stringify(callbackPayload));
} else {
window.webkit!.messageHandlers.revokeExternalAuth.postMessage(
callbackPayload

View File

@@ -7,6 +7,9 @@
/** Icon to use when no icon specified for domain. */
export const DEFAULT_DOMAIN_ICON = "hass:bookmark";
/** Panel to show when no panel is picked. */
export const DEFAULT_PANEL = "states";
/** Domains that have a state card. */
export const DOMAINS_WITH_CARD = [
"climate",

View File

@@ -5,87 +5,82 @@ import formatDate from "../datetime/format_date";
import formatTime from "../datetime/format_time";
import { LocalizeFunc } from "../../mixins/localize-base-mixin";
type CachedDisplayEntity = HassEntity & {
_stateDisplay?: string;
};
export default function computeStateDisplay(
export default (
localize: LocalizeFunc,
stateObj: HassEntity,
language: string
) {
const state = stateObj as CachedDisplayEntity;
if (!state._stateDisplay) {
const domain = computeStateDomain(state);
if (domain === "binary_sensor") {
// Try device class translation, then default binary sensor translation
if (state.attributes.device_class) {
state._stateDisplay = localize(
`state.${domain}.${state.attributes.device_class}.${state.state}`
);
}
if (!state._stateDisplay) {
state._stateDisplay = localize(
`state.${domain}.default.${state.state}`
);
}
} else if (
state.attributes.unit_of_measurement &&
!["unknown", "unavailable"].includes(state.state)
) {
state._stateDisplay =
state.state + " " + state.attributes.unit_of_measurement;
} else if (domain === "input_datetime") {
let date: Date;
if (!state.attributes.has_time) {
date = new Date(
state.attributes.year,
state.attributes.month - 1,
state.attributes.day
);
state._stateDisplay = formatDate(date, language);
} else if (!state.attributes.has_date) {
const now = new Date();
date = new Date(
// Due to bugs.chromium.org/p/chromium/issues/detail?id=797548
// don't use artificial 1970 year.
now.getFullYear(),
now.getMonth(),
now.getDay(),
state.attributes.hour,
state.attributes.minute
);
state._stateDisplay = formatTime(date, language);
} else {
date = new Date(
state.attributes.year,
state.attributes.month - 1,
state.attributes.day,
state.attributes.hour,
state.attributes.minute
);
state._stateDisplay = formatDateTime(date, language);
}
} else if (domain === "zwave") {
if (["initializing", "dead"].includes(state.state)) {
state._stateDisplay = localize(
`state.zwave.query_stage.${state.state}`,
"query_stage",
state.attributes.query_stage
);
} else {
state._stateDisplay = localize(`state.zwave.default.${state.state}`);
}
} else {
state._stateDisplay = localize(`state.${domain}.${state.state}`);
): string => {
let display: string | undefined;
const domain = computeStateDomain(stateObj);
if (domain === "binary_sensor") {
// Try device class translation, then default binary sensor translation
if (stateObj.attributes.device_class) {
display = localize(
`state.${domain}.${stateObj.attributes.device_class}.${stateObj.state}`
);
}
// Fall back to default, component backend translation, or raw state if nothing else matches.
state._stateDisplay =
state._stateDisplay ||
localize(`state.default.${state.state}`) ||
localize(`component.${domain}.state.${state.state}`) ||
state.state;
if (!display) {
display = localize(`state.${domain}.default.${stateObj.state}`);
}
} else if (
stateObj.attributes.unit_of_measurement &&
!["unknown", "unavailable"].includes(stateObj.state)
) {
display = stateObj.state + " " + stateObj.attributes.unit_of_measurement;
} else if (domain === "input_datetime") {
let date: Date;
if (!stateObj.attributes.has_time) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day
);
display = formatDate(date, language);
} else if (!stateObj.attributes.has_date) {
const now = new Date();
date = new Date(
// Due to bugs.chromium.org/p/chromium/issues/detail?id=797548
// don't use artificial 1970 year.
now.getFullYear(),
now.getMonth(),
now.getDay(),
stateObj.attributes.hour,
stateObj.attributes.minute
);
display = formatTime(date, language);
} else {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
);
display = formatDateTime(date, language);
}
} else if (domain === "zwave") {
if (["initializing", "dead"].includes(stateObj.state)) {
display = localize(
`state.zwave.query_stage.${stateObj.state}`,
"query_stage",
stateObj.attributes.query_stage
);
} else {
display = localize(`state.zwave.default.${stateObj.state}`);
}
} else {
display = localize(`state.${domain}.${stateObj.state}`);
}
return state._stateDisplay;
}
// Fall back to default, component backend translation, or raw state if nothing else matches.
if (!display) {
display =
localize(`state.default.${stateObj.state}`) ||
localize(`component.${domain}.state.${stateObj.state}`) ||
stateObj.state;
}
return display;
};

View File

@@ -1,18 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import computeObjectId from "./compute_object_id";
type CachedDisplayEntity = HassEntity & {
_entityDisplay?: string;
};
export default function computeStateName(stateObj: HassEntity) {
const state = stateObj as CachedDisplayEntity;
if (state._entityDisplay === undefined) {
state._entityDisplay =
state.attributes.friendly_name ||
computeObjectId(state.entity_id).replace(/_/g, " ");
}
return state._entityDisplay;
}
export default (stateObj: HassEntity): string =>
stateObj.attributes.friendly_name === undefined
? computeObjectId(stateObj.entity_id).replace(/_/g, " ")
: stateObj.attributes.friendly_name || "";

View File

@@ -1,24 +1,30 @@
export default function parseAspectRatio(input) {
// Handle 16x9, 16:9, 1.78x1, 1.78:1, 1.78
// Ignore everything else
function parseOrThrow(num) {
const parsed = parseFloat(num);
if (isNaN(parsed)) {
throw new Error(`${num} is not a number`);
}
return parsed;
// Handle 16x9, 16:9, 1.78x1, 1.78:1, 1.78
// Ignore everything else
const parseOrThrow = (num) => {
const parsed = parseFloat(num);
if (isNaN(parsed)) {
throw new Error(`${num} is not a number`);
}
return parsed;
};
export default function parseAspectRatio(input: string) {
if (!input) {
return null;
}
try {
if (input) {
const arr = input.replace(":", "x").split("x");
if (arr.length === 0) {
return null;
}
return arr.length === 1
? { w: parseOrThrow(arr[0]), h: 1 }
: { w: parseOrThrow(arr[0]), h: parseOrThrow(arr[1]) };
if (input.endsWith("%")) {
return { w: 100, h: parseOrThrow(input.substr(0, input.length - 1)) };
}
const arr = input.replace(":", "x").split("x");
if (arr.length === 0) {
return null;
}
return arr.length === 1
? { w: parseOrThrow(arr[0]), h: 1 }
: { w: parseOrThrow(arr[0]), h: parseOrThrow(arr[1]) };
} catch (err) {
// Ignore the error
}

View File

@@ -0,0 +1,3 @@
export const afterNextRender = (cb: () => void): void => {
requestAnimationFrame(() => setTimeout(cb, 0));
};

View File

@@ -1,25 +1,208 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../ha-label-badge";
import {
LitElement,
html,
PropertyValues,
PropertyDeclarations,
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { HassEntity } from "home-assistant-js-websocket";
import { classMap } from "lit-html/directives/classMap";
import computeStateDomain from "../../common/entity/compute_state_domain";
import computeStateName from "../../common/entity/compute_state_name";
import domainIcon from "../../common/entity/domain_icon";
import stateIcon from "../../common/entity/state_icon";
import timerTimeRemaining from "../../common/entity/timer_time_remaining";
import attributeClassNames from "../../common/entity/attribute_class_names";
import secondsToDuration from "../../common/datetime/seconds_to_duration";
import { fireEvent } from "../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../mixins/lit-localize-mixin";
import EventsMixin from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
import "../ha-label-badge";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HaStateLabelBadge extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
export class HaStateLabelBadge extends hassLocalizeLitMixin(LitElement) {
public state?: HassEntity;
private _connected?: boolean;
private _updateRemaining?: number;
private _timerTimeRemaining?: number;
public connectedCallback(): void {
super.connectedCallback();
this._connected = true;
this.startInterval(this.state);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._connected = false;
this.clearInterval();
}
protected render(): TemplateResult {
const state = this.state;
if (!state) {
return html`
${this.renderStyle()}
<ha-label-badge label="not found"></ha-label-badge>
`;
}
const domain = computeStateDomain(state);
return html`
${this.renderStyle()}
<ha-label-badge
class="${
classMap({
[domain]: true,
"has-unit_of_measurement":
"unit_of_measurement" in state.attributes,
})
}"
.value="${this._computeValue(domain, state)}"
.icon="${this._computeIcon(domain, state)}"
.image="${state.attributes.entity_picture}"
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
.description="${computeStateName(state)}"
></ha-label-badge>
`;
}
static get properties(): PropertyDeclarations {
return {
hass: {},
state: {},
_timerTimeRemaining: {},
};
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.addEventListener("click", (ev) => {
ev.stopPropagation();
if (this.state) {
fireEvent(this, "hass-more-info", { entityId: this.state.entity_id });
}
});
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (this._connected && changedProperties.has("state")) {
this.startInterval(this.state);
}
}
private _computeValue(domain: string, state: HassEntity) {
switch (domain) {
case "binary_sensor":
case "device_tracker":
case "updater":
case "sun":
case "alarm_control_panel":
case "timer":
return null;
case "sensor":
default:
return state.state === "unknown"
? "-"
: this.localize(`component.${domain}.state.${state.state}`) ||
state.state;
}
}
private _computeIcon(domain: string, state: HassEntity) {
if (state.state === "unavailable") {
return null;
}
switch (domain) {
case "alarm_control_panel":
if (state.state === "pending") {
return "hass:clock-fast";
}
if (state.state === "armed_away") {
return "hass:nature";
}
if (state.state === "armed_home") {
return "hass:home-variant";
}
if (state.state === "armed_night") {
return "hass:weather-night";
}
if (state.state === "armed_custom_bypass") {
return "hass:security-home";
}
if (state.state === "triggered") {
return "hass:alert-circle";
}
// state == 'disarmed'
return domainIcon(domain, state.state);
case "binary_sensor":
case "device_tracker":
case "updater":
return stateIcon(state);
case "sun":
return state.state === "above_horizon"
? domainIcon(domain)
: "hass:brightness-3";
case "timer":
return state.state === "active" ? "hass:timer" : "hass:timer-off";
default:
return null;
}
}
private _computeLabel(domain, state, _timerTimeRemaining) {
if (
state.state === "unavailable" ||
["device_tracker", "alarm_control_panel"].includes(domain)
) {
// Localize the state with a special state_badge namespace, which has variations of
// the state translations that are truncated to fit within the badge label. Translations
// are only added for device_tracker and alarm_control_panel.
return (
this.localize(`state_badge.${domain}.${state.state}`) ||
this.localize(`state_badge.default.${state.state}`) ||
state.state
);
}
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}
return state.attributes.unit_of_measurement || null;
}
private clearInterval() {
if (this._updateRemaining) {
clearInterval(this._updateRemaining);
this._updateRemaining = undefined;
}
}
private startInterval(stateObj) {
this.clearInterval();
if (stateObj && computeStateDomain(stateObj) === "timer") {
this.calculateTimerRemaining(stateObj);
if (stateObj.state === "active") {
this._updateRemaining = window.setInterval(
() => this.calculateTimerRemaining(this.state),
1000
);
}
}
}
private calculateTimerRemaining(stateObj) {
this._timerTimeRemaining = timerTimeRemaining(stateObj);
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
@@ -61,175 +244,13 @@ class HaStateLabelBadge extends LocalizeMixin(EventsMixin(PolymerElement)) {
);
}
</style>
<ha-label-badge
class$="[[computeClassNames(state)]]"
value="[[computeValue(localize, state)]]"
icon="[[computeIcon(state)]]"
image="[[computeImage(state)]]"
label="[[computeLabel(localize, state, _timerTimeRemaining)]]"
description="[[computeDescription(state)]]"
></ha-label-badge>
`;
}
}
static get properties() {
return {
hass: Object,
state: {
type: Object,
observer: "stateChanged",
},
_timerTimeRemaining: {
type: Number,
value: 0,
},
};
}
connectedCallback() {
super.connectedCallback();
this.startInterval(this.state);
}
disconnectedCallback() {
super.disconnectedCallback();
this.clearInterval();
}
ready() {
super.ready();
this.addEventListener("click", (ev) => this.badgeTap(ev));
}
badgeTap(ev) {
ev.stopPropagation();
this.fire("hass-more-info", { entityId: this.state.entity_id });
}
computeClassNames(state) {
const classes = [computeStateDomain(state)];
classes.push(attributeClassNames(state, ["unit_of_measurement"]));
return classes.join(" ");
}
computeValue(localize, state) {
const domain = computeStateDomain(state);
switch (domain) {
case "binary_sensor":
case "device_tracker":
case "updater":
case "sun":
case "alarm_control_panel":
case "timer":
return null;
case "sensor":
default:
return state.state === "unknown"
? "-"
: localize(`component.${domain}.state.${state.state}`) || state.state;
}
}
computeIcon(state) {
if (state.state === "unavailable") {
return null;
}
const domain = computeStateDomain(state);
switch (domain) {
case "alarm_control_panel":
if (state.state === "pending") {
return "hass:clock-fast";
}
if (state.state === "armed_away") {
return "hass:nature";
}
if (state.state === "armed_home") {
return "hass:home-variant";
}
if (state.state === "armed_night") {
return "hass:weather-night";
}
if (state.state === "armed_custom_bypass") {
return "hass:security-home";
}
if (state.state === "triggered") {
return "hass:alert-circle";
}
// state == 'disarmed'
return domainIcon(domain, state.state);
case "binary_sensor":
case "device_tracker":
case "updater":
return stateIcon(state);
case "sun":
return state.state === "above_horizon"
? domainIcon(domain)
: "hass:brightness-3";
case "timer":
return state.state === "active" ? "hass:timer" : "hass:timer-off";
default:
return null;
}
}
computeImage(state) {
return state.attributes.entity_picture || null;
}
computeLabel(localize, state, _timerTimeRemaining) {
const domain = computeStateDomain(state);
if (
state.state === "unavailable" ||
["device_tracker", "alarm_control_panel"].includes(domain)
) {
// Localize the state with a special state_badge namespace, which has variations of
// the state translations that are truncated to fit within the badge label. Translations
// are only added for device_tracker and alarm_control_panel.
return (
localize(`state_badge.${domain}.${state.state}`) ||
localize(`state_badge.default.${state.state}`) ||
state.state
);
}
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}
return state.attributes.unit_of_measurement || null;
}
computeDescription(state) {
return computeStateName(state);
}
stateChanged(stateObj) {
this.updateStyles();
this.startInterval(stateObj);
}
clearInterval() {
if (this._updateRemaining) {
clearInterval(this._updateRemaining);
this._updateRemaining = null;
}
}
startInterval(stateObj) {
this.clearInterval();
if (computeStateDomain(stateObj) === "timer") {
this.calculateTimerRemaining(stateObj);
if (stateObj.state === "active") {
this._updateRemaining = setInterval(
() => this.calculateTimerRemaining(this.state),
1000
);
}
}
}
calculateTimerRemaining(stateObj) {
this._timerTimeRemaining = timerTimeRemaining(stateObj);
declare global {
interface HTMLElementTagNameMap {
"ha-state-label-badge": HaStateLabelBadge;
}
}

View File

@@ -81,8 +81,15 @@ class HaClimateControl extends EventsMixin(PolymerElement) {
this.$.target_temperature.classList.toggle("in-flux", inFlux);
}
_round(val) {
// round value to precision derived from step
// insired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
const s = this.step.toString().split(".");
return s[1] ? parseFloat(val.toFixed(s[1].length)) : Math.round(val);
}
incrementValue() {
const newval = this.value + this.step;
const newval = this._round(this.value + this.step);
if (this.value < this.max) {
this.last_changed = Date.now();
this.temperatureStateInFlux(true);
@@ -102,7 +109,7 @@ class HaClimateControl extends EventsMixin(PolymerElement) {
}
decrementValue() {
const newval = this.value - this.step;
const newval = this._round(this.value - this.step);
if (this.value > this.min) {
this.last_changed = Date.now();
this.temperatureStateInFlux(true);

View File

@@ -1,9 +1,83 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import {
LitElement,
PropertyDeclarations,
PropertyValues,
} from "@polymer/lit-element";
import { TemplateResult, html } from "lit-html";
import { classMap } from "lit-html/directives/classMap";
import "./ha-icon";
class HaLabelBadge extends PolymerElement {
static get template() {
class HaLabelBadge extends LitElement {
public value?: string;
public icon?: string;
public label?: string;
public description?: string;
public image?: string;
static get properties(): PropertyDeclarations {
return {
value: {},
icon: {},
label: {},
description: {},
image: {},
};
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<div class="badge-container">
<div class="label-badge" id="badge">
<div
class="${
classMap({
value: true,
big: Boolean(this.value && this.value.length > 4),
})
}"
>
${
this.icon && !this.value && !this.image
? html`
<ha-icon .icon="${this.icon}"></ha-icon>
`
: ""
}
${
this.value && !this.image
? html`
<span>${this.value}</span>
`
: ""
}
</div>
${
this.label
? html`
<div
class="${
classMap({ label: true, big: this.label.length > 5 })
}"
>
<span>${this.label}</span>
</div>
`
: ""
}
</div>
${
this.description
? html`
<div class="title">${this.description}</div>
`
: ""
}
</div>
`;
}
protected renderStyle(): TemplateResult {
return html`
<style>
.badge-container {
@@ -74,69 +148,25 @@ class HaLabelBadge extends PolymerElement {
text-overflow: ellipsis;
line-height: normal;
}
[hidden] {
display: none !important;
}
</style>
<div class="badge-container">
<div class="label-badge" id="badge">
<div class$="[[computeValueClasses(value)]]">
<ha-icon
icon="[[icon]]"
hidden$="[[computeHideIcon(icon, value, image)]]"
></ha-icon>
<span hidden$="[[computeHideValue(value, image)]]">[[value]]</span>
</div>
<div
hidden$="[[computeHideLabel(label)]]"
class$="[[computeLabelClasses(label)]]"
>
<span>[[label]]</span>
</div>
</div>
<div class="title" hidden$="[[!description]]">[[description]]</div>
</div>
`;
}
static get properties() {
return {
value: String,
icon: String,
label: String,
description: String,
image: {
type: String,
observer: "imageChanged",
},
};
}
computeValueClasses(value) {
return value && value.length > 4 ? "value big" : "value";
}
computeLabelClasses(label) {
return label && label.length > 5 ? "label big" : "label";
}
computeHideLabel(label) {
return !label || !label.trim();
}
computeHideIcon(icon, value, image) {
return !icon || value || image;
}
computeHideValue(value, image) {
return !value || image;
}
imageChanged(newVal) {
this.$.badge.style.backgroundImage = newVal ? "url(" + newVal + ")" : "";
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("image")) {
this.shadowRoot!.getElementById("badge")!.style.backgroundImage = this
.image
? `url(${this.image})`
: "";
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-label-badge": HaLabelBadge;
}
}
customElements.define("ha-label-badge", HaLabelBadge);

View File

@@ -0,0 +1,18 @@
import { HomeAssistant } from "../types";
export const callAlarmAction = (
hass: HomeAssistant,
entity: string,
action:
| "arm_away"
| "arm_home"
| "arm_night"
| "arm_custom_bypass"
| "disarm",
code: string
) => {
hass!.callService("alarm_control_panel", "alarm_" + action, {
entity_id: entity,
code,
});
};

View File

@@ -1,22 +1,28 @@
import { HomeAssistant } from "../types";
export interface LovelaceConfig {
_frontendAuto: boolean;
title?: string;
views: LovelaceViewConfig[];
background?: string;
resources?: Array<{ type: "css" | "js" | "module" | "html"; url: string }>;
excluded_entities?: string[];
}
export interface LovelaceViewConfig {
index?: number;
title?: string;
badges?: string[];
cards?: LovelaceCardConfig[];
id?: string;
path?: string;
icon?: string;
theme?: string;
panel?: boolean;
background?: string;
}
export interface LovelaceCardConfig {
id?: string;
index?: number;
view_index?: number;
type: string;
[key: string]: any;
}
@@ -60,95 +66,11 @@ export const fetchConfig = (
force,
});
export const migrateConfig = (hass: HomeAssistant): Promise<void> =>
hass.callWS({
type: "lovelace/config/migrate",
});
export const saveConfig = (
hass: HomeAssistant,
config: LovelaceConfig | string,
format: "json" | "yaml"
config: LovelaceConfig
): Promise<void> =>
hass.callWS({
type: "lovelace/config/save",
config,
format,
});
export const getCardConfig = (
hass: HomeAssistant,
cardId: string
): Promise<string> =>
hass.callWS({
type: "lovelace/config/card/get",
card_id: cardId,
});
export const updateCardConfig = (
hass: HomeAssistant,
cardId: string,
config: LovelaceCardConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/card/update",
card_id: cardId,
card_config: config,
format,
});
export const deleteCard = (
hass: HomeAssistant,
cardId: string
): Promise<void> =>
hass.callWS({
type: "lovelace/config/card/delete",
card_id: cardId,
});
export const addCard = (
hass: HomeAssistant,
viewId: string,
config: LovelaceCardConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/card/add",
view_id: viewId,
card_config: config,
format,
});
export const updateViewConfig = (
hass: HomeAssistant,
viewId: string,
config: LovelaceViewConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/update",
view_id: viewId,
view_config: config,
format,
});
export const deleteView = (
hass: HomeAssistant,
viewId: string
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/delete",
view_id: viewId,
});
export const addView = (
hass: HomeAssistant,
config: LovelaceViewConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/add",
view_config: config,
format,
});

View File

@@ -0,0 +1,6 @@
export const ERR_ID_REUSE = "id_reuse";
export const ERR_INVALID_FORMAT = "invalid_format";
export const ERR_NOT_FOUND = "not_found";
export const ERR_UNKNOWN_COMMAND = "unknown_command";
export const ERR_UNKNOWN_ERROR = "unknown_error";
export const ERR_UNAUTHORIZED = "unauthorized";

View File

@@ -1,15 +1,18 @@
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 LocalizeMixin from "../../../mixins/localize-mixin";
class MoreInfoScript extends PolymerElement {
class MoreInfoScript extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex iron-flex-alignment"></style>
<div class="layout vertical">
<div class="data-entry layout justified horizontal">
<div class="key">Last Action</div>
<div class="key">
[[localize('ui.dialogs.more_info_control.script.last_action')]]
</div>
<div class="value">[[stateObj.attributes.last_action]]</div>
</div>
</div>

View File

@@ -28,7 +28,9 @@ class MoreInfoSun extends LocalizeMixin(PolymerElement) {
</div>
</template>
<div class="data-entry layout justified horizontal">
<div class="key">Elevation</div>
<div class="key">
[[localize('ui.dialogs.more_info_control.sun.elevation')]]
</div>
<div class="value">[[stateObj.attributes.elevation]]</div>
</div>
`;
@@ -63,7 +65,10 @@ class MoreInfoSun extends LocalizeMixin(PolymerElement) {
}
itemCaption(type) {
return type === "ris" ? "Rising " : "Setting ";
if (type === "ris") {
return this.localize("ui.dialogs.more_info_control.sun.rising");
}
return this.localize("ui.dialogs.more_info_control.sun.setting");
}
itemDate(type) {

View File

@@ -1,7 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import LocalizeMixin from "../../../mixins/localize-mixin";
class MoreInfoUpdater extends PolymerElement {
class MoreInfoUpdater extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style>
@@ -15,7 +16,7 @@ class MoreInfoUpdater extends PolymerElement {
class="link"
href="https://www.home-assistant.io/docs/installation/updating/"
target="_blank"
>Update Instructions</a
>[[localize('ui.dialogs.more_info_control.updater.title')]]</a
>
</div>
`;

View File

@@ -26,7 +26,7 @@ const isExternal = location.search.includes("external_auth=1");
const authProm = isExternal
? () =>
import("../common/auth/external_auth").then(
import(/* webpackChunkName: "external_auth" */ "../common/auth/external_auth").then(
(mod) => new mod.default(hassUrl)
)
: () =>

View File

@@ -56,6 +56,10 @@ export default (superClass) =>
dockedSidebar: false,
moreInfoEntityId: null,
callService: async (domain, service, serviceData = {}) => {
if (__DEV__) {
// eslint-disable-next-line
console.log("Calling service", domain, service, serviceData);
}
try {
await callService(conn, domain, service, serviceData);
@@ -91,6 +95,15 @@ export default (superClass) =>
}
this.fire("hass-notification", { message });
} catch (err) {
if (__DEV__) {
// eslint-disable-next-line
console.error(
"Error calling service",
domain,
service,
serviceData
);
}
const message = this.localize(
"ui.notification_toast.service_call_failed",
"service",
@@ -106,21 +119,25 @@ export default (superClass) =>
fetchWithAuth(auth, `${auth.data.hassUrl}${path}`, init),
// For messages that do not get a response
sendWS: (msg) => {
// eslint-disable-next-line
if (__DEV__) console.log("Sending", msg);
if (__DEV__) {
// eslint-disable-next-line
console.log("Sending", msg);
}
conn.sendMessage(msg);
},
// For messages that expect a response
callWS: (msg) => {
/* eslint-disable no-console */
if (__DEV__) console.log("Sending", msg);
if (__DEV__) {
/* eslint-disable no-console */
console.log("Sending", msg);
}
const resp = conn.sendMessagePromise(msg);
if (__DEV__) {
resp.then(
(result) => console.log("Received", result),
(err) => console.log("Error", err)
(err) => console.error("Error", err)
);
}
return resp;

View File

@@ -10,6 +10,7 @@ import "../home-assistant-main";
import "../ha-init-page";
import "../../resources/ha-style";
import registerServiceWorker from "../../util/register-service-worker";
import { DEFAULT_PANEL } from "../../common/const";
import HassBaseMixin from "./hass-base-mixin";
import AuthMixin from "./auth-mixin";
@@ -94,7 +95,7 @@ class HomeAssistant extends ext(PolymerElement, [
}
computePanelUrl(routeData) {
return (routeData && routeData.panel) || "lovelace";
return (routeData && routeData.panel) || DEFAULT_PANEL;
}
panelUrlChanged(newPanelUrl) {

View File

@@ -11,6 +11,7 @@ import "./partial-panel-resolver";
import EventsMixin from "../mixins/events-mixin";
import NavigateMixin from "../mixins/navigate-mixin";
import { computeRTL } from "../common/util/compute_rtl";
import { DEFAULT_PANEL } from "../common/const";
import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar");
import(/* webpackChunkName: "voice-command-dialog" */ "../dialogs/ha-voice-command-dialog");
@@ -98,7 +99,7 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
ready() {
super.ready();
this._defaultPage = localStorage.defaultPage || "lovelace";
this._defaultPage = localStorage.defaultPage || DEFAULT_PANEL;
this.addEventListener("hass-open-menu", () => this.handleOpenMenu());
this.addEventListener("hass-close-menu", () => this.handleCloseMenu());
this.addEventListener("hass-start-voice", (ev) =>
@@ -135,7 +136,7 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
connectedCallback() {
super.connectedCallback();
if (document.location.pathname === "/") {
this.navigate(`/${localStorage.defaultPage || "lovelace"}`, true);
this.navigate(`/${localStorage.defaultPage || DEFAULT_PANEL}`, true);
}
}

View File

@@ -37,13 +37,15 @@ export const hassLocalizeLitMixin = <T extends LitElement>(
public connectedCallback(): void {
super.connectedCallback();
let language;
let resources;
if (this.hass) {
language = this.hass.language;
resources = this.hass.resources;
if (this.localize === empty) {
let language;
let resources;
if (this.hass) {
language = this.hass.language;
resources = this.hass.resources;
}
this.localize = this.__computeLocalize(language, resources);
}
this.localize = this.__computeLocalize(language, resources);
}
public updated(changedProperties: PropertyValues) {

View File

@@ -1,4 +1,5 @@
import IntlMessageFormat from "intl-messageformat/src/main";
import { HomeAssistant } from "../types";
/**
* Adapted from Polymer app-localize-behavior.
@@ -32,6 +33,7 @@ export interface FormatsType {
export type LocalizeFunc = (key: string, ...args: any[]) => string;
export interface LocalizeMixin {
hass?: HomeAssistant;
localize: LocalizeFunc;
}

View File

@@ -70,6 +70,7 @@ export class CloudExposedEntities extends LitElement {
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (
changedProperties.has("filter") &&
changedProperties.get("filter") !== this.filter

View File

@@ -20,6 +20,7 @@ import {
deleteCloudhook,
CloudWebhook,
} from "../../../data/cloud";
import { ERR_UNKNOWN_COMMAND } from "../../../data/websocket_api";
declare global {
// for fire event
@@ -78,6 +79,7 @@ export class CloudWebhooks extends LitElement {
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("cloudStatus") && this.cloudStatus) {
this._cloudHooks = this.cloudStatus.prefs.cloudhooks || {};
}
@@ -86,7 +88,17 @@ export class CloudWebhooks extends LitElement {
private _renderBody() {
if (!this.cloudStatus || !this._localHooks || !this._cloudHooks) {
return html`
<div class="loading">Loading…</div>
<div class="body-text">Loading…</div>
`;
}
if (this._localHooks.length === 0) {
return html`
<div class="body-text">
Looks like you have no webhooks yet. Get started by configuring a
<a href="/config/integrations">webhook-based integration</a> or by
creating a <a href="/config/automation/new">webhook automation</a>.
</div>
`;
}
@@ -188,7 +200,15 @@ export class CloudWebhooks extends LitElement {
}
private async _fetchData() {
this._localHooks = await fetchWebhooks(this.hass!);
try {
this._localHooks = await fetchWebhooks(this.hass!);
} catch (err) {
if (err.code === ERR_UNKNOWN_COMMAND) {
this._localHooks = [];
} else {
throw err;
}
}
}
private renderStyle() {
@@ -197,7 +217,7 @@ export class CloudWebhooks extends LitElement {
.body {
padding: 0 16px 8px;
}
.loading {
.body-text {
padding: 0 16px;
}
.webhook {
@@ -217,6 +237,7 @@ export class CloudWebhooks extends LitElement {
.footer {
padding: 16px;
}
.body-text a,
.footer a {
color: var(--primary-color);
}

View File

@@ -169,7 +169,8 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
fireEvent(this, "register-dialog", {
dialogShowEvent: "manage-cloud-webhook",
dialogTag: "cloud-webhook-manage-dialog",
dialogImport: () => import("./cloud-webhook-manage-dialog"),
dialogImport: () =>
import(/* webpackChunkName: "cloud-webhook-manage-dialog" */ "./cloud-webhook-manage-dialog"),
});
}
}

View File

@@ -172,7 +172,8 @@ class HaConfigManagerDashboard extends LocalizeMixin(
this.fire("register-dialog", {
dialogShowEvent: "show-config-flow",
dialogTag: "ha-config-flow",
dialogImport: () => import("./ha-config-flow"),
dialogImport: () =>
import(/* webpackChunkName: "ha-config-flow" */ "./ha-config-flow"),
});
}
}

View File

@@ -50,7 +50,7 @@ class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
pages: {
type: Array,
value: ["core", "customize", "automation", "script", "zwave"],
value: ["core", "customize", "automation", "script", "zha", "zwave"],
},
};
}

View File

@@ -17,6 +17,7 @@ import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-c
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard");
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script");
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users");
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha");
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave");
/*
@@ -106,6 +107,18 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
></ha-config-script>
</template>
<template
is="dom-if"
if="[[_equals(_routeData.page, &quot;zha&quot;)]]"
restamp
>
<ha-config-zha
page-name="zha"
hass="[[hass]]"
is-wide="[[isWide]]"
></ha-config-zha>
</template>
<template
is="dom-if"
if="[[_equals(_routeData.page, &quot;zwave&quot;)]]"

View File

@@ -12,6 +12,7 @@ import StateTrigger from "./state";
import SunTrigger from "./sun";
import TemplateTrigger from "./template";
import TimeTrigger from "./time";
import WebhookTrigger from "./webhook";
import ZoneTrigger from "./zone";
const TYPES = {
@@ -23,6 +24,7 @@ const TYPES = {
sun: SunTrigger,
template: TemplateTrigger,
time: TimeTrigger,
webhook: WebhookTrigger,
zone: ZoneTrigger,
};

View File

@@ -0,0 +1,32 @@
import { h, Component } from "preact";
import "@polymer/paper-input/paper-input";
import { onChangeEvent } from "../../../../common/preact/event";
export default class WebhookTrigger extends Component {
constructor() {
super();
this.onChange = onChangeEvent.bind(this, "trigger");
}
render({ trigger, localize }) {
const { webhook_id: webhookId } = trigger;
return (
<div>
<paper-input
label={localize(
"ui.panel.config.automation.editor.triggers.type.webhook.webhook_id"
)}
name="webhook_id"
value={webhookId}
onvalue-changed={this.onChange}
/>
</div>
);
}
}
WebhookTrigger.defaultConfig = {
webhook_id: "",
};

View File

@@ -90,7 +90,8 @@ class HaUserPicker extends EventsMixin(
this.fire("register-dialog", {
dialogShowEvent: "show-add-user",
dialogTag: "ha-dialog-add-user",
dialogImport: () => import("./ha-dialog-add-user"),
dialogImport: () =>
import(/* webpackChunkName: "ha-dialog-add-user" */ "./ha-dialog-add-user"),
});
}
}

View File

@@ -0,0 +1,80 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { HomeAssistant } from "../../../types";
import "../../../layouts/ha-app-layout";
import "../../../resources/ha-style";
import "./zha-network";
export class HaConfigZha extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _haStyle?: DocumentFragment;
private _ironFlex?: DocumentFragment;
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
};
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<ha-app-layout has-scrolling-region="">
<app-header slot="header" fixed="">
<app-toolbar>
<paper-icon-button
icon="hass:arrow-left"
@click="${this._onBackTapped}"
></paper-icon-button>
</app-toolbar>
</app-header>
<zha-network
id="zha-network"
.is-wide="${this.isWide}"
.hass="${this.hass}"
></zha-network>
</ha-app-layout>
`;
}
private renderStyle(): TemplateResult {
if (!this._haStyle) {
this._haStyle = document.importNode(
(document.getElementById("ha-style")!
.children[0] as HTMLTemplateElement).content,
true
);
}
if (!this._ironFlex) {
this._ironFlex = document.importNode(
(document.getElementById("iron-flex")!
.children[0] as HTMLTemplateElement).content,
true
);
}
return html`
${this._ironFlex} ${this._haStyle}
`;
}
private _onBackTapped(): void {
history.back();
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-zha": HaConfigZha;
}
}
customElements.define("ha-config-zha", HaConfigZha);

View File

@@ -0,0 +1,129 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-card/paper-card";
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-service-description";
import "../ha-config-section";
import { HomeAssistant } from "../../../types";
import "../../../resources/ha-style";
export class ZHANetwork extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
public showDescription: boolean;
private _haStyle?: DocumentFragment;
private _ironFlex?: DocumentFragment;
constructor() {
super();
this.showDescription = false;
}
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
showDescription: {},
};
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<ha-config-section .is-wide="${this.isWide}">
<div style="position: relative" slot="header">
<span>Zigbee Home Automation network management</span>
<paper-icon-button class="toggle-help-icon" @click="${
this._onHelpTap
}" icon="hass:help-circle"></paper-icon-button>
</div>
<span slot="introduction">Commands that affect entire network</span>
<paper-card class="content">
<div class="card-actions">
<ha-call-service-button .hass="${
this.hass
}" domain="zha" service="permit">Permit</ha-call-service-button>
${
this.showDescription
? html`
<ha-service-description
.hass="${this.hass}"
domain="zha"
service="permit"
/>
`
: ""
}
</paper-card>
</ha-config-section>
`;
}
private _onHelpTap(): void {
this.showDescription = !this.showDescription;
}
private renderStyle(): TemplateResult {
if (!this._haStyle) {
this._haStyle = document.importNode(
(document.getElementById("ha-style")!
.children[0] as HTMLTemplateElement).content,
true
);
}
if (!this._ironFlex) {
this._ironFlex = document.importNode(
(document.getElementById("iron-flex")!
.children[0] as HTMLTemplateElement).content,
true
);
}
return html`
${this._ironFlex} ${this._haStyle}
<style>
.content {
margin-top: 24px;
}
paper-card {
display: block;
margin: 0 auto;
max-width: 600px;
}
.card-actions.warning ha-call-service-button {
color: var(--google-red-500);
}
.toggle-help-icon {
position: absolute;
top: -6px;
right: 0;
color: var(--primary-color);
}
ha-service-description {
display: block;
color: grey;
}
[hidden] {
display: none;
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-network": ZHANetwork;
}
}
customElements.define("zha-network", ZHANetwork);

View File

@@ -108,6 +108,16 @@ class ZwaveGroups extends PolymerElement {
Remove From Group
</ha-call-service-button>
</template>
<template is="dom-if" if="[[_isBroadcastNodeInGroup]]">
<ha-call-service-button
hass="[[hass]]"
domain="zwave"
service="change_association"
service-data="[[_removeBroadcastNodeServiceData]]"
>
Remove Broadcast
</ha-call-service-button>
</template>
</div>
</template>
</paper-card>
@@ -165,6 +175,16 @@ class ZwaveGroups extends PolymerElement {
type: String,
value: "",
},
_removeBroadcastNodeServiceData: {
type: String,
value: "",
},
_isBroadcastNodeInGroup: {
type: Boolean,
value: false,
},
};
}
@@ -201,6 +221,7 @@ class ZwaveGroups extends PolymerElement {
_computeOtherGroupNodes(selectedGroup) {
if (selectedGroup === -1) return -1;
this.setProperties({ _isBroadcastNodeInGroup: false });
const associations = Object.values(
this.groups[selectedGroup].value.association_instances
);
@@ -212,6 +233,17 @@ class ZwaveGroups extends PolymerElement {
const id = assoc[0];
const instance = assoc[1];
const node = this.nodes.find((n) => n.attributes.node_id === id);
if (id === 255) {
this.setProperties({
_isBroadcastNodeInGroup: true,
_removeBroadcastNodeServiceData: {
node_id: this.nodes[this.selectedNode].attributes.node_id,
association: "remove",
target_node_id: 255,
group: this.groups[selectedGroup].key,
},
});
}
if (!node) {
return `Unknown Node (${id}: (${instance} ? ${id}.${instance} : ${id}))`;
}
@@ -288,6 +320,7 @@ class ZwaveGroups extends PolymerElement {
_otherGroupNodes: Object.values(
groupData[this._selectedGroup].value.associations
),
_isBroadcastNodeInGroup: false,
});
const oldGroup = this._selectedGroup;
this.setProperties({ _selectedGroup: -1 });

View File

@@ -133,7 +133,8 @@ class OzwLog extends EventsMixin(PolymerElement) {
this.fire("register-dialog", {
dialogShowEvent: "show-ozwlog-dialog",
dialogTag: "zwave-log-dialog",
dialogImport: () => import("./zwave-log-dialog"),
dialogImport: () =>
import(/* webpackChunkName: "zwave-log-dialog" */ "./zwave-log-dialog"),
});
}
}

View File

@@ -20,6 +20,7 @@ import formatTime from "../../common/datetime/format_time";
import EventsMixin from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
const OPT_IN_PANEL = "lovelace";
let registeredDialog = false;
class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
@@ -164,7 +165,7 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
</template>
</p>
<p>
<a href='/states'>Go back to the old states page</a>
<a href='/lovelace'>Try out the new Lovelace UI</a>
<div id="love" style="cursor:pointer;" on-click="_toggleDefaultPage">[[_defaultPageText()]]</div
</p>
</div>
@@ -311,7 +312,8 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
this.fire("register-dialog", {
dialogShowEvent: "show-loaded-components",
dialogTag: "ha-loaded-components",
dialogImport: () => import("./ha-loaded-components"),
dialogImport: () =>
import(/* webpackChunkName: "ha-loaded-components" */ "./ha-loaded-components"),
});
}
@@ -364,15 +366,15 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {
_defaultPageText() {
return `>> ${
localStorage.defaultPage === "states" ? "Remove" : "Set"
} the old states as default page on this device <<`;
localStorage.defaultPage === OPT_IN_PANEL ? "Remove" : "Set"
} ${OPT_IN_PANEL} as default page on this device <<`;
}
_toggleDefaultPage() {
if (localStorage.defaultPage === "states") {
if (localStorage.defaultPage === OPT_IN_PANEL) {
delete localStorage.defaultPage;
} else {
localStorage.defaultPage = "states";
localStorage.defaultPage = OPT_IN_PANEL;
}
this.$.love.innerText = this._defaultPageText();
}

View File

@@ -28,6 +28,7 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
.content {
padding: 16px;
direction: ltr;
}
ha-entity-picker,

View File

@@ -1,258 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../components/ha-label-badge";
/*
* @appliesMixin EventsMixin
*/
const Icons = {
armed_away: "hass:security-lock",
armed_custom_bypass: "hass:security",
armed_home: "hass:security-home",
armed_night: "hass:security-home",
disarmed: "hass:verified",
pending: "hass:shield-outline",
triggered: "hass:bell-ring",
};
class HuiAlarmPanelCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style>
ha-card {
padding-bottom: 16px;
position: relative;
--alarm-color-disarmed: var(--label-badge-green);
--alarm-color-pending: var(--label-badge-yellow);
--alarm-color-triggered: var(--label-badge-red);
--alarm-color-armed: var(--label-badge-red);
--alarm-color-autoarm: rgba(0, 153, 255, .1);
--alarm-state-color: var(--alarm-color-armed);
--base-unit: 15px;
font-size: calc(var(--base-unit));
}
ha-label-badge {
--ha-label-badge-color: var(--alarm-state-color);
--label-badge-text-color: var(--alarm-state-color);
color: var(--alarm-state-color);
position: absolute;
right: 12px;
top: 12px;
}
.disarmed {
--alarm-state-color: var(--alarm-color-disarmed);
}
.triggered {
--alarm-state-color: var(--alarm-color-triggered);
animation: pulse 1s infinite;
}
.arming {
--alarm-state-color: var(--alarm-color-pending);
animation: pulse 1s infinite;
}
.pending {
--alarm-state-color: var(--alarm-color-pending);
animation: pulse 1s infinite;
}
@keyframes pulse {
0% {
--ha-label-badge-color: var(--alarm-state-color);
}
100% {
--ha-label-badge-color: rgba(255, 153, 0, 0.3);
}
}
paper-input {
margin: auto;
max-width: 200px;
font-size: calc(var(--base-unit));
}
.state {
margin-left: 16px;
font-size: calc(var(--base-unit) * 0.9);
position: relative;
bottom: 16px;
color: var(--alarm-state-color);
animation: none;
}
#keypad {
display: flex;
justify-content: center;
}
#keypad div {
display: flex;
flex-direction: column;
}
#keypad paper-button {
margin-bottom: 10%;
position: relative;
padding: calc(var(--base-unit));
font-size: calc(var(--base-unit) * 1.1);
}
.actions {
margin: 0 8px;
padding-top: 20px;
display: flex;
flex-wrap: wrap;
justify-content: center;
font-size: calc(var(--base-unit) * 1);
}
.actions paper-button {
min-width: calc(var(--base-unit) * 9);
color: var(--primary-color);
}
paper-button#disarm {
color: var(--google-red-500);
}
.not-found {
flex: 1;
background-color: yellow;
padding: 8px;
}
</style>
<ha-card
header$="[[_computeHeader(localize, _stateObj)]]"
class$="[[_computeClassName(_stateObj)]]"
>
<template is="dom-if" if="[[_stateObj]]">
<ha-label-badge
class$="[[_stateObj.state]]"
icon="[[_computeIcon(_stateObj)]]"
label="[[_stateIconLabel(_stateObj.state)]]"
></ha-label-badge>
<template is="dom-if" if="[[_showActionToggle(_stateObj.state)]]">
<div id="armActions" class="actions">
<template is="dom-repeat" items="[[_config.states]]">
<paper-button noink raised id="[[item]]" on-click="_handleActionClick">[[_label(localize, item)]]</paper-button>
</template>
</div>
</template>
<template is="dom-if" if="[[!_showActionToggle(_stateObj.state)]]">
<div id="disarmActions" class="actions">
<paper-button noink raised id="disarm" on-click="_handleActionClick">[[_label(localize, "disarm")]]</paper-button>
</div>
</template>
<paper-input label="Alarm Code" type="password" value="[[_value]]"></paper-input>
<div id="keypad">
<div>
<paper-button noink raised value="1" on-click="_handlePadClick">1</paper-button>
<paper-button noink raised value="4" on-click="_handlePadClick">4</paper-button>
<paper-button noink raised value="7" on-click="_handlePadClick">7</paper-button>
</div>
<div>
<paper-button noink raised value="2" on-click="_handlePadClick">2</paper-button>
<paper-button noink raised value="5" on-click="_handlePadClick">5</paper-button>
<paper-button noink raised value="8" on-click="_handlePadClick">8</paper-button>
<paper-button noink raised value="0" on-click="_handlePadClick">0</paper-button>
</div>
<div>
<paper-button noink raised value="3" on-click="_handlePadClick">3</paper-button>
<paper-button noink raised value="6" on-click="_handlePadClick">6</paper-button>
<paper-button noink raised value="9" on-click="_handlePadClick">9</paper-button>
<paper-button noink raised value="clear" on-click="_handlePadClick">[[_label(localize, "clear_code")]]</paper-button>
</div>
</template>
<template is="dom-if" if="[[!_stateObj]]">
<div>Entity not available: [[_config.entity]]</div>
</template>
</ha-card>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
_config: Object,
_stateObj: {
type: Object,
computed: "_computeStateObj(hass.states, _config.entity)",
},
_value: {
type: String,
value: "",
},
};
}
getCardSize() {
return 4;
}
setConfig(config) {
if (
!config ||
!config.entity ||
config.entity.split(".")[0] !== "alarm_control_panel"
) {
throw new Error("Invalid card configuration");
}
const defaults = {
states: ["arm_away", "arm_home"],
};
this._config = { ...defaults, ...config };
this._icons = Icons;
}
_computeStateObj(states, entityId) {
return states && entityId in states ? states[entityId] : null;
}
_computeHeader(localize, stateObj) {
if (!stateObj) return "";
return this._config.name
? this._config.name
: this._label(localize, stateObj.state);
}
_computeIcon(stateObj) {
return this._icons[stateObj.state] || "hass:shield-outline";
}
_label(localize, state) {
return (
localize(`state.alarm_control_panel.${state}`) ||
localize(`ui.card.alarm_control_panel.${state}`)
);
}
_stateIconLabel(state) {
const stateLabel = state.split("_").pop();
return stateLabel === "disarmed" || stateLabel === "triggered"
? ""
: stateLabel;
}
_showActionToggle(state) {
return state === "disarmed";
}
_computeClassName(stateObj) {
if (!stateObj) return "not-found";
return "";
}
_handlePadClick(e) {
const val = e.target.getAttribute("value");
this._value = val === "clear" ? "" : this._value + val;
}
_handleActionClick(e) {
this.hass.callService("alarm_control_panel", "alarm_" + e.target.id, {
entity_id: this._stateObj.entity_id,
code: this._value,
});
this._value = "";
}
}
customElements.define("hui-alarm-panel-card", HuiAlarmPanelCard);

View File

@@ -0,0 +1,316 @@
import {
html,
LitElement,
PropertyValues,
PropertyDeclarations,
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { classMap } from "lit-html/directives/classMap";
import { LovelaceCard } from "../types";
import { HomeAssistant } from "../../../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { callAlarmAction } from "../../../data/alarm_control_panel";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import "../../../components/ha-card";
import "../../../components/ha-label-badge";
import {
createErrorCardConfig,
createErrorCardElement,
} from "./hui-error-card";
const ICONS = {
armed_away: "hass:security-lock",
armed_custom_bypass: "hass:security",
armed_home: "hass:security-home",
armed_night: "hass:security-home",
disarmed: "hass:verified",
pending: "hass:shield-outline",
triggered: "hass:bell-ring",
};
const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"];
export interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
states?: string[];
}
class HuiAlarmPanelCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement() {
await import(/* webpackChunkName: "hui-alarm-panel-card-editor" */ "../editor/config-elements/hui-alarm-panel-card-editor");
return document.createElement("hui-alarm-panel-card-editor");
}
public static getStubConfig() {
return { states: ["arm_home", "arm_away"] };
}
public hass?: HomeAssistant;
private _config?: Config;
private _code?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
_code: {},
};
}
public getCardSize(): number {
return 4;
}
public setConfig(config: Config): void {
if (
!config ||
!config.entity ||
config.entity.split(".")[0] !== "alarm_control_panel"
) {
throw new Error("Invalid card configuration");
}
const defaults = {
states: ["arm_away", "arm_home"],
};
this._config = { ...defaults, ...config };
this._code = "";
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_config") || changedProps.has("_code")) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass) {
return (
oldHass.states[this._config!.entity] !==
this.hass!.states[this._config!.entity]
);
}
return true;
}
protected render(): TemplateResult {
if (!this._config || !this.hass) {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
const element = createErrorCardElement(
createErrorCardConfig("Entity not Found!", this._config)
);
return html`
${element}
`;
}
return html`
${this.renderStyle()}
<ha-card .header="${this._config.name || this._label(stateObj.state)}">
<ha-label-badge
class="${classMap({ [stateObj.state]: true })}"
.icon="${ICONS[stateObj.state] || "hass:shield-outline"}"
.label="${this._stateIconLabel(stateObj.state)}"
></ha-label-badge>
<div id="armActions" class="actions">
${
(stateObj.state === "disarmed"
? this._config.states!
: ["disarm"]
).map((state) => {
return html`
<paper-button
noink
raised
.action="${state}"
@click="${this._handleActionClick}"
>${this._label(state)}</paper-button
>
`;
})
}
</div>
${
!stateObj.attributes.code_format
? html``
: html`
<paper-input
label="Alarm Code"
type="password"
.value="${this._code}"
></paper-input>
`
}
${
stateObj.attributes.code_format !== "Number"
? html``
: html`
<div id="keypad">
${
BUTTONS.map((value) => {
return value === ""
? html`
<paper-button disabled></paper-button>
`
: html`
<paper-button
noink
raised
.value="${value}"
@click="${this._handlePadClick}"
>${
value === "clear"
? this._label("clear_code")
: value
}</paper-button
>
`;
})
}
</div>
`
}
</ha-card>
`;
}
private _stateIconLabel(state: string): string {
const stateLabel = state.split("_").pop();
return stateLabel === "disarmed" ||
stateLabel === "triggered" ||
!stateLabel
? ""
: stateLabel;
}
private _label(state: string): string {
return (
this.localize(`state.alarm_control_panel.${state}`) ||
this.localize(`ui.card.alarm_control_panel.${state}`)
);
}
private _handlePadClick(e: MouseEvent): void {
const val = (e.currentTarget! as any).value;
this._code = val === "clear" ? "" : this._code + val;
}
private _handleActionClick(e: MouseEvent): void {
callAlarmAction(
this.hass!,
this._config!.entity_id,
(e.currentTarget! as any).action,
this._code!
);
this._code = "";
}
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
padding-bottom: 16px;
position: relative;
--alarm-color-disarmed: var(--label-badge-green);
--alarm-color-pending: var(--label-badge-yellow);
--alarm-color-triggered: var(--label-badge-red);
--alarm-color-armed: var(--label-badge-red);
--alarm-color-autoarm: rgba(0, 153, 255, 0.1);
--alarm-state-color: var(--alarm-color-armed);
--base-unit: 15px;
font-size: calc(var(--base-unit));
}
ha-label-badge {
--ha-label-badge-color: var(--alarm-state-color);
--label-badge-text-color: var(--alarm-state-color);
--label-badge-background-color: var(--paper-card-background-color);
color: var(--alarm-state-color);
position: absolute;
right: 12px;
top: 12px;
}
.disarmed {
--alarm-state-color: var(--alarm-color-disarmed);
}
.triggered {
--alarm-state-color: var(--alarm-color-triggered);
animation: pulse 1s infinite;
}
.arming {
--alarm-state-color: var(--alarm-color-pending);
animation: pulse 1s infinite;
}
.pending {
--alarm-state-color: var(--alarm-color-pending);
animation: pulse 1s infinite;
}
@keyframes pulse {
0% {
--ha-label-badge-color: var(--alarm-state-color);
}
100% {
--ha-label-badge-color: rgba(255, 153, 0, 0.3);
}
}
paper-input {
margin: 0 auto 8px;
max-width: 150px;
font-size: calc(var(--base-unit));
text-align: center;
}
.state {
margin-left: 16px;
font-size: calc(var(--base-unit) * 0.9);
position: relative;
bottom: 16px;
color: var(--alarm-state-color);
animation: none;
}
#keypad {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin: auto;
width: 300px;
}
#keypad paper-button {
margin-bottom: 5%;
width: 30%;
padding: calc(var(--base-unit));
font-size: calc(var(--base-unit) * 1.1);
}
.actions {
margin: 0 8px;
padding-top: 20px;
display: flex;
flex-wrap: wrap;
justify-content: center;
font-size: calc(var(--base-unit) * 1);
}
.actions paper-button {
min-width: calc(var(--base-unit) * 9);
color: var(--primary-color);
}
paper-button#disarm {
color: var(--google-red-500);
}
</style>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-alarm-panel-card": HuiAlarmPanelCard;
}
}
customElements.define("hui-alarm-panel-card", HuiAlarmPanelCard);

View File

@@ -1,4 +1,4 @@
import createCardElement from "../common/create-card-element";
import { createCardElement } from "../common/create-card-element";
import { computeCardSize } from "../common/compute-card-size";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";

View File

@@ -17,7 +17,7 @@ import { EntityConfig, EntityRow } from "../entity-rows/types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { processConfigEntities } from "../common/process-config-entities";
import createRowElement from "../common/create-row-element";
import { createRowElement } from "../common/create-row-element";
import computeDomain from "../../../common/entity/compute_domain";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
@@ -40,7 +40,7 @@ export interface Config extends LovelaceCardConfig {
class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import("../editor/config-elements/hui-entities-card-editor");
await import(/* webpackChunkName: "hui-entities-card-editor" */ "../editor/config-elements/hui-entities-card-editor");
return document.createElement("hui-entities-card-editor");
}
@@ -88,7 +88,8 @@ class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement)
this._configEntities = entities;
}
protected updated(_changedProperties: PropertyValues): void {
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (this._hass && this._config) {
applyThemesOnElement(this, this._hass.themes, this._config.theme);
}

View File

@@ -17,12 +17,12 @@ import computeStateName from "../../../common/entity/compute_state_name";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { longPress } from "../common/directives/long-press-directive";
import { handleClick } from "../common/handle-click";
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
icon?: string;
@@ -33,6 +33,18 @@ interface Config extends LovelaceCardConfig {
class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-entity-button-card-editor" */ "../editor/config-elements/hui-entity-button-card-editor");
return document.createElement("hui-entity-button-card-editor");
}
public static getStubConfig(): object {
return {
tap_action: { action: "more-info" },
hold_action: { action: "none" },
};
}
public hass?: HomeAssistant;
private _config?: Config;
@@ -116,6 +128,7 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}

View File

@@ -1,6 +1,6 @@
import { PolymerElement } from "@polymer/polymer/polymer-element";
import createCardElement from "../common/create-card-element";
import { createCardElement } from "../common/create-card-element";
import { processConfigEntities } from "../common/process-config-entities";
function getEntities(hass, filterState, entities) {

View File

@@ -3,13 +3,27 @@ import { html, LitElement } from "@polymer/lit-element";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
interface Config extends LovelaceCardConfig {
error: string;
origConfig: LovelaceCardConfig;
}
class HuiErrorCard extends LitElement implements LovelaceCard {
export const createErrorCardElement = (config) => {
const el = document.createElement("hui-error-card");
el.setConfig(config);
return el;
};
export const createErrorCardConfig = (error, origConfig) => ({
type: "error",
error,
origConfig,
});
export class HuiErrorCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant;
private _config?: Config;
static get properties() {

View File

@@ -5,30 +5,40 @@ import {
PropertyValues,
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { styleMap } from "lit-html/directives/styleMap";
import { LovelaceCard } from "../types";
import "../../../components/ha-card";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import isValidEntityId from "../../../common/entity/valid_entity_id";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import computeStateName from "../../../common/entity/compute_state_name";
import "../../../components/ha-card";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import {
createErrorCardConfig,
createErrorCardElement,
} from "./hui-error-card";
interface Config extends LovelaceCardConfig {
export interface SeverityConfig {
green?: number;
yellow?: number;
red?: number;
}
export interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
unit?: string;
min?: number;
max?: number;
severity?: object;
severity?: SeverityConfig;
theme?: string;
}
const severityMap = {
export const severityMap = {
red: "var(--label-badge-red)",
green: "var(--label-badge-green)",
yellow: "var(--label-badge-yellow)",
@@ -36,6 +46,14 @@ const severityMap = {
};
class HuiGaugeCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-gauge-card-editor" */ "../editor/config-elements/hui-gauge-card-editor");
return document.createElement("hui-gauge-card-editor");
}
public static getStubConfig(): object {
return {};
}
public hass?: HomeAssistant;
private _config?: Config;
@@ -65,11 +83,23 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
let state;
let error;
if (!stateObj) {
error = "Entity not available: " + this._config.entity;
} else if (isNaN(Number(stateObj.state))) {
error = "Entity is non-numeric: " + this._config.entity;
} else {
state = Number(stateObj.state);
if (isNaN(state)) {
error = "Entity is non-numeric: " + this._config.entity;
}
}
if (error) {
return html`
${createErrorCardElement(createErrorCardConfig(error, this._config))}
`;
}
return html`
@@ -84,7 +114,15 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
<div class="container">
<div class="gauge-a"></div>
<div class="gauge-b"></div>
<div class="gauge-c" id="gauge"></div>
<div
class="gauge-c"
style="${
styleMap({
transform: `rotate(${this._translateTurn(state)}turn)`,
"background-color": this._computeSeverity(state),
})
}"
></div>
<div class="gauge-data">
<div id="percent">
${stateObj.state}
@@ -109,41 +147,74 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
return hasConfigOrEntityChanged(this, changedProps);
}
protected updated(changedProps: PropertyValues): void {
if (
!this._config ||
!this.hass ||
!this.shadowRoot!.getElementById("gauge")
) {
return;
}
const stateObj = this.hass.states[this._config.entity];
if (isNaN(Number(stateObj.state))) {
return;
}
const turn = this._translateTurn(Number(stateObj.state), this._config);
this.shadowRoot!.getElementById(
"gauge"
)!.style.cssText = `transform: rotate(${turn}turn); background-color: ${this._computeSeverity(
stateObj.state,
this._config.severity!
)}`;
protected firstUpdated(): void {
(this.shadowRoot!.querySelector(
"ha-card"
)! as HTMLElement).style.setProperty(
"--base-unit",
this._computeBaseUnit()
);
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
private _computeSeverity(numberValue: number): string {
const sections = this._config!.severity;
if (!sections) {
return severityMap.normal;
}
const sectionsArray = Object.keys(sections);
const sortable = sectionsArray.map((severity) => [
severity,
sections[severity],
]);
for (const severity of sortable) {
if (severityMap[severity[0]] == null || isNaN(severity[1])) {
return severityMap.normal;
}
}
sortable.sort((a, b) => a[1] - b[1]);
if (numberValue >= sortable[0][1] && numberValue < sortable[1][1]) {
return severityMap[sortable[0][0]];
}
if (numberValue >= sortable[1][1] && numberValue < sortable[2][1]) {
return severityMap[sortable[1][0]];
}
if (numberValue >= sortable[2][1]) {
return severityMap[sortable[2][0]];
}
return severityMap.normal;
}
private _translateTurn(value: number): number {
const { min, max } = this._config!;
const maxTurnValue = Math.min(Math.max(value, min!), max!);
return (5 * (maxTurnValue - min!)) / (max! - min!) / 10;
}
private _computeBaseUnit(): string {
return this.clientWidth < 200 ? this.clientWidth / 5 + "px" : "50px";
}
private _handleClick(): void {
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
}
private renderStyle(): TemplateResult {
return html`
<style>
@@ -226,53 +297,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
</style>
`;
}
private _computeSeverity(stateValue: string, sections: object): string {
const numberValue = Number(stateValue);
if (!sections) {
return severityMap.normal;
}
const sectionsArray = Object.keys(sections);
const sortable = sectionsArray.map((severity) => [
severity,
sections[severity],
]);
for (const severity of sortable) {
if (severityMap[severity[0]] == null || isNaN(severity[1])) {
return severityMap.normal;
}
}
sortable.sort((a, b) => a[1] - b[1]);
if (numberValue >= sortable[0][1] && numberValue < sortable[1][1]) {
return severityMap[sortable[0][0]];
}
if (numberValue >= sortable[1][1] && numberValue < sortable[2][1]) {
return severityMap[sortable[1][0]];
}
if (numberValue >= sortable[2][1]) {
return severityMap[sortable[2][0]];
}
return severityMap.normal;
}
private _translateTurn(value: number, config: Config): number {
const maxTurnValue = Math.min(Math.max(value, config.min!), config.max!);
return (
(5 * (maxTurnValue - config.min!)) / (config.max! - config.min!) / 10
);
}
private _computeBaseUnit(): string {
return this.clientWidth < 200 ? this.clientWidth / 5 + "px" : "50px";
}
private _handleClick(): void {
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
}
}
declare global {

View File

@@ -41,7 +41,7 @@ export interface Config extends LovelaceCardConfig {
export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import("../editor/config-elements/hui-glance-card-editor");
await import(/* webpackChunkName: "hui-glance-card-editor" */ "../editor/config-elements/hui-glance-card-editor");
return document.createElement("hui-glance-card-editor");
}
public static getStubConfig(): object {
@@ -135,6 +135,7 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (!this._config || !this.hass) {
return;
}

View File

@@ -2,18 +2,26 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "../../../components/ha-card";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
import { styleMap } from "lit-html/directives/styleMap";
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
aspect_ratio?: string;
title?: string;
url: string;
}
export class HuiIframeCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-iframe-card-editor" */ "../editor/config-elements/hui-iframe-card-editor");
return document.createElement("hui-iframe-card-editor");
}
public static getStubConfig(): object {
return { url: "https://www.home-assistant.io", aspect_ratio: "50%" };
}
protected _config?: Config;
static get properties(): PropertyDeclarations {

View File

@@ -1,4 +1,4 @@
import createErrorCardConfig from "../common/create-error-card-config";
import { createErrorCardConfig } from "./hui-error-card";
import computeDomain from "../../../common/entity/compute_domain";
export default class LegacyWrapperCard extends HTMLElement {

View File

@@ -10,7 +10,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { styleMap } from "lit-html/directives/styleMap";
import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { longPress } from "../common/directives/long-press-directive";
import { hasConfigOrEntityChanged } from "../common/has-changed";
@@ -36,9 +36,10 @@ const lightConfig = {
lineCap: "round",
handleSize: "+12",
showTooltip: false,
animation: false,
};
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
theme?: string;
@@ -46,6 +47,14 @@ interface Config extends LovelaceCardConfig {
export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-light-card-editor" */ "../editor/config-elements/hui-light-card-editor");
return document.createElement("hui-light-card-editor");
}
public static getStubConfig(): object {
return {};
}
public hass?: HomeAssistant;
private _config?: Config;
private _brightnessTimout?: number;
@@ -134,8 +143,14 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
this._roundSliderStyle = loaded.roundSliderStyle;
this._jQuery = loaded.jQuery;
const brightness = this.hass!.states[this._config!.entity].attributes
.brightness;
const stateObj = this.hass!.states[this._config!.entity] as LightEntity;
if (!stateObj) {
return;
}
const brightness = stateObj.attributes.brightness || 0;
this._jQuery("#light", this.shadowRoot).roundSlider({
...lightConfig,
change: (value) => this._setBrightness(value),
@@ -148,11 +163,18 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass || !this._jQuery) {
return;
}
const attrs = this.hass!.states[this._config!.entity].attributes;
const stateObj = this.hass!.states[this._config!.entity];
if (!stateObj) {
return;
}
const attrs = stateObj.attributes;
this._jQuery("#light", this.shadowRoot).roundSlider({
value: Math.round((attrs.brightness / 254) * 100) || 0,

View File

@@ -11,7 +11,24 @@ import computeStateName from "../../../common/entity/compute_state_name";
import debounce from "../../../common/util/debounce";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
// should be interface when converted to TS
export const Config = {
title: "",
aspect_ratio: "",
default_zoom: 14,
entities: [],
};
class HuiMapCard extends PolymerElement {
static async getConfigElement() {
await import(/* webpackChunkName: "hui-map-card-editor" */ "../editor/config-elements/hui-map-card-editor");
return document.createElement("hui-map-card-editor");
}
static getStubConfig() {
return { entities: [] };
}
static get template() {
return html`
<style>
@@ -111,8 +128,24 @@ class HuiMapCard extends PolymerElement {
throw new Error("Error in card configuration.");
}
this._configEntities = processConfigEntities(config.entities);
if (!config.entities && !config.geo_location_sources) {
throw new Error(
"Either entities or geo_location_sources must be defined"
);
}
if (config.entities && !Array.isArray(config.entities)) {
throw new Error("Entities need to be an array");
}
if (
config.geo_location_sources &&
!Array.isArray(config.geo_location_sources)
) {
throw new Error("Geo_location_sources needs to be an array");
}
this._config = config;
this._configGeoLocationSources = config.geo_location_sources;
this._configEntities = config.entities;
}
getCardSize() {
@@ -205,7 +238,24 @@ class HuiMapCard extends PolymerElement {
}
const mapItems = (this._mapItems = []);
this._configEntities.forEach((entity) => {
let allEntities = [];
if (this._configEntities) {
allEntities = allEntities.concat(this._configEntities);
}
if (this._configGeoLocationSources) {
Object.keys(this.hass.states).forEach((entityId) => {
const stateObj = this.hass.states[entityId];
if (
computeStateDomain(stateObj) === "geo_location" &&
this._configGeoLocationSources.includes(stateObj.attributes.source)
) {
allEntities.push(entityId);
}
});
}
allEntities = processConfigEntities(allEntities);
allEntities.forEach((entity) => {
const entityId = entity.entity;
if (!(entityId in hass.states)) {
return;

View File

@@ -4,16 +4,24 @@ import { classMap } from "lit-html/directives/classMap";
import "../../../components/ha-card";
import "../../../components/ha-markdown";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { TemplateResult } from "lit-html";
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
content: string;
title?: string;
}
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-markdown-card-editor" */ "../editor/config-elements/hui-markdown-card-editor");
return document.createElement("hui-markdown-card-editor");
}
public static getStubConfig(): object {
return { content: " " };
}
private _config?: Config;
static get properties(): PropertyDeclarations {

View File

@@ -2,7 +2,21 @@ import "../../../cards/ha-media_player-card";
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
// should be interface when converted to TS
export const Config = {
entity: "",
};
class HuiMediaControlCard extends LegacyWrapperCard {
static async getConfigElement() {
await import(/* webpackChunkName: "hui-media-control-card-editor" */ "../editor/config-elements/hui-media-control-card-editor");
return document.createElement("hui-media-control-card-editor");
}
static getStubConfig() {
return {};
}
constructor() {
super("ha-media_player-card", "media_player");
}

View File

@@ -2,7 +2,7 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "../../../components/ha-card";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { TemplateResult } from "lit-html";
@@ -10,20 +10,31 @@ import { classMap } from "lit-html/directives/classMap";
import { handleClick } from "../common/handle-click";
import { longPress } from "../common/directives/long-press-directive";
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
image?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
export class HuiPictureCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-picture-card-editor" */ "../editor/config-elements/hui-picture-card-editor");
return document.createElement("hui-picture-card-editor");
}
public static getStubConfig(): object {
return {
image:
"https://www.home-assistant.io/images/merchandise/shirt-frontpage.png",
tap_action: { action: "none" },
hold_action: { action: "none" },
};
}
public hass?: HomeAssistant;
protected _config?: Config;
static get properties(): PropertyDeclarations {
return {
_config: {},
};
return { _config: {} };
}
public getCardSize(): number {

View File

@@ -1,7 +1,7 @@
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import createHuiElement from "../common/create-hui-element";
import { createHuiElement } from "../common/create-hui-element";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
@@ -63,15 +63,15 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
return html`
${this.renderStyle()}
<ha-card .header="${this._config.title}">
<hui-image
.hass="${this._hass}"
.image="${this._config.image}"
.stateImage="${this._config.state_image}"
.cameraImage="${this._config.camera_image}"
.entity="${this._config.entity}"
.aspectRatio="${this._config.aspect_ratio}"
></hui-image>
<div id="root">
<hui-image
.hass="${this._hass}"
.image="${this._config.image}"
.stateImage="${this._config.state_image}"
.cameraImage="${this._config.camera_image}"
.entity="${this._config.entity}"
.aspectRatio="${this._config.aspect_ratio}"
></hui-image>
${
this._config.elements.map((elementConfig: LovelaceElementConfig) =>
this._createHuiElement(elementConfig)
@@ -85,9 +85,9 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
private renderStyle(): TemplateResult {
return html`
<style>
ha-card {
overflow: hidden;
#root {
position: relative;
overflow: hidden;
}
.element {
position: absolute;

View File

@@ -16,6 +16,10 @@ import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { LovelaceCard } from "../types";
import { handleClick } from "../common/handle-click";
import { UNAVAILABLE } from "../../../data/entity";
import {
createErrorCardElement,
createErrorCardConfig,
} from "./hui-error-card";
interface Config extends LovelaceCardConfig {
entity: string;
@@ -62,11 +66,25 @@ class HuiPictureEntityCard extends hassLocalizeLitMixin(LitElement)
}
protected render(): TemplateResult {
if (!this._config || !this.hass || !this.hass.states[this._config.entity]) {
if (!this._config || !this.hass) {
return html``;
}
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
${
createErrorCardElement(
createErrorCardConfig(
`Entity not found: ${this._config.entity}`,
this._config
)
)
}
`;
}
const name = this._config.name || computeStateName(stateObj);
const state = computeStateDisplay(
this.localize,

View File

@@ -2,7 +2,22 @@ import "../../../cards/ha-plant-card";
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
// should be interface when converted to TS
export const Config = {
name: "",
entity: "",
};
class HuiPlantStatusCard extends LegacyWrapperCard {
static async getConfigElement() {
await import(/* webpackChunkName: "hui-plant-status-card-editor" */ "../editor/config-elements/hui-plant-status-card-editor");
return document.createElement("hui-plant-status-card-editor");
}
static getStubConfig() {
return {};
}
constructor() {
super("ha-plant-card", "plant");
}

View File

@@ -8,7 +8,7 @@ import {
import { TemplateResult } from "lit-html";
import "@polymer/paper-spinner/paper-spinner";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -133,7 +133,7 @@ const coordinates = (
return calcPoints(history, hours, width, detail, min, max);
};
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
entity: string;
name?: string;
icon?: string;
@@ -145,6 +145,15 @@ interface Config extends LovelaceCardConfig {
}
class HuiSensorCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-sensor-card-editor" */ "../editor/config-elements/hui-sensor-card-editor");
return document.createElement("hui-sensor-card-editor");
}
public static getStubConfig(): object {
return {};
}
public hass?: HomeAssistant;
private _config?: Config;
private _history?: any;
@@ -265,6 +274,7 @@ class HuiSensorCard extends LitElement implements LovelaceCard {
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (!this._config || this._config.graph !== "line" || !this.hass) {
return;
}

View File

@@ -9,7 +9,7 @@ import "../../../components/ha-icon";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import {
fetchItems,
@@ -19,12 +19,20 @@ import {
addItem,
} from "../../../data/shopping-list";
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
title?: string;
}
class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-shopping-list-editor" */ "../editor/config-elements/hui-shopping-list-editor");
return document.createElement("hui-shopping-list-card-editor");
}
public static getStubConfig(): object {
return {};
}
public hass?: HomeAssistant;
private _config?: Config;
private _uncheckedItems?: ShoppingListItem[];
@@ -33,6 +41,7 @@ class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
static get properties() {
return {
hass: {},
_config: {},
_uncheckedItems: {},
_checkedItems: {},
@@ -117,7 +126,7 @@ class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
<paper-item-body>
<paper-input
no-label-float
value="${item.name}"
.value="${item.name}"
.itemId="${item.id}"
@change="${this._saveEdit}"
></paper-input>
@@ -168,7 +177,7 @@ class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement)
<paper-item-body>
<paper-input
no-label-float
value="${item.name}"
.value="${item.name}"
.itemId="${item.id}"
@change="${this._saveEdit}"
></paper-input>

View File

@@ -1,7 +1,7 @@
import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import createCardElement from "../common/create-card-element";
import { createCardElement } from "../common/create-card-element";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";

View File

@@ -7,28 +7,30 @@ import {
import { classMap } from "lit-html/directives/classMap";
import { TemplateResult } from "lit-html";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import computeStateName from "../../../common/entity/compute_state_name";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { HomeAssistant, ClimateEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard } from "../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { loadRoundslider } from "../../../resources/jquery.roundslider.ondemand";
import { afterNextRender } from "../../../common/util/render-status";
import { UNIT_F } from "../../../common/const";
const thermostatConfig = {
radius: 150,
step: 1,
circleShape: "pie",
startAngle: 315,
width: 5,
lineCap: "round",
handleSize: "+10",
showTooltip: false,
animation: false,
};
const modeIcons = {
@@ -43,7 +45,7 @@ const modeIcons = {
idle: "hass:power-sleep",
};
interface Config extends LovelaceCardConfig {
export interface Config extends LovelaceCardConfig {
entity: string;
theme?: string;
name?: string;
@@ -53,12 +55,30 @@ function formatTemp(temps: string[]): string {
return temps.filter(Boolean).join("-");
}
function computeTemperatureStepSize(hass: HomeAssistant, config: Config) {
const stateObj = hass.states[config.entity];
if (stateObj.attributes.target_temp_step) {
return stateObj.attributes.target_temp_step;
}
return hass.config.unit_system.temperature === UNIT_F ? 1 : 0.5;
}
export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import(/* webpackChunkName: "hui-thermostat-card-editor" */ "../editor/config-elements/hui-thermostat-card-editor");
return document.createElement("hui-thermostat-card-editor");
}
public static getStubConfig(): object {
return { entity: "" };
}
public hass?: HomeAssistant;
private _config?: Config;
private _roundSliderStyle?: TemplateResult;
private _jQuery?: any;
private _broadCard?: boolean;
static get properties(): PropertyDeclarations {
return {
@@ -86,7 +106,6 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
return html``;
}
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
const broadCard = this.clientWidth > 390;
const mode = modeIcons[stateObj.attributes.operation_mode || ""]
? stateObj.attributes.operation_mode!
: "unknown-mode";
@@ -95,8 +114,8 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
<ha-card
class="${classMap({
[mode]: true,
large: broadCard,
small: !broadCard,
large: this._broadCard!,
small: !this._broadCard,
})}">
<div id="root">
<div id="thermostat"></div>
@@ -137,8 +156,49 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
return hasConfigOrEntityChanged(this, changedProps);
}
protected async firstUpdated(): Promise<void> {
protected firstUpdated(): void {
this._initialLoad();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass || !changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
if (
this._jQuery &&
// If jQuery changed, we just rendered in firstUpdated
!changedProps.has("_jQuery") &&
(!oldHass || oldHass.states[this._config.entity] !== stateObj)
) {
const [sliderValue, uiValue] = this._genSliderValue(stateObj);
this._jQuery("#thermostat", this.shadowRoot).roundSlider({
value: sliderValue,
});
this._updateSetTemp(uiValue);
}
}
private async _initialLoad(): Promise<void> {
const radius = this.clientWidth / 3;
this._broadCard = this.clientWidth > 390;
(this.shadowRoot!.querySelector(
"#thermostat"
)! as HTMLElement).style.minHeight = radius * 2 + "px";
const loaded = await loadRoundslider();
await new Promise((resolve) => afterNextRender(resolve));
this._roundSliderStyle = loaded.roundSliderStyle;
this._jQuery = loaded.jQuery;
@@ -151,26 +211,27 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
? "range"
: "min-range";
const [sliderValue, uiValue] = this._genSliderValue(stateObj);
const step = computeTemperatureStepSize(this.hass!, this._config!);
this._jQuery("#thermostat", this.shadowRoot).roundSlider({
...thermostatConfig,
radius: this.clientWidth / 3,
radius,
min: stateObj.attributes.min_temp,
max: stateObj.attributes.max_temp,
sliderType: _sliderType,
create: () => this._loaded(),
change: (value) => this._setTemperature(value),
drag: (value) => this._dragEvent(value),
value: sliderValue,
step,
});
this._updateSetTemp(uiValue);
}
protected updated(changedProps: PropertyValues): void {
if (!this._config || !this.hass || !this._jQuery) {
return;
}
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
let sliderValue;
let uiValue;
private _genSliderValue(stateObj: ClimateEntity): [string | number, string] {
let sliderValue: string | number;
let uiValue: string;
if (
stateObj.attributes.target_temp_low &&
@@ -184,18 +245,73 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
String(stateObj.attributes.target_temp_high),
]);
} else {
sliderValue = uiValue = stateObj.attributes.temperature;
sliderValue = stateObj.attributes.temperature;
uiValue = "" + stateObj.attributes.temperature;
}
this._jQuery("#thermostat", this.shadowRoot).roundSlider({
value: sliderValue,
return [sliderValue, uiValue];
}
private _loaded(): void {
(this.shadowRoot!.querySelector(
"#thermostat"
)! as HTMLElement).style.minHeight = null;
}
private _updateSetTemp(value: string): void {
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = value;
}
private _dragEvent(e): void {
this._updateSetTemp(formatTemp(String(e.value).split(",")));
}
private _setTemperature(e): void {
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
if (
stateObj.attributes.target_temp_low &&
stateObj.attributes.target_temp_high
) {
if (e.handle.index === 1) {
this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity,
target_temp_low: e.handle.value,
target_temp_high: stateObj.attributes.target_temp_high,
});
} else {
this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity,
target_temp_low: stateObj.attributes.target_temp_low,
target_temp_high: e.handle.value,
});
}
} else {
this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity,
temperature: e.value,
});
}
}
private _renderIcon(mode: string, currentMode: string): TemplateResult {
if (!modeIcons[mode]) {
return html``;
}
return html`
<ha-icon
class="${classMap({ "selected-icon": currentMode === mode })}"
.mode="${mode}"
.icon="${modeIcons[mode]}"
@click="${this._handleModeClick}"
></ha-icon>
`;
}
private _handleModeClick(e: MouseEvent): void {
this.hass!.callService("climate", "set_operation_mode", {
entity_id: this._config!.entity,
operation_mode: (e.currentTarget as any).mode,
});
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = uiValue;
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes !== this.hass.themes) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
private renderStyle(): TemplateResult {
@@ -373,60 +489,6 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
</style>
`;
}
private _dragEvent(e): void {
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = formatTemp(
String(e.value).split(",")
);
}
private _setTemperature(e): void {
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
if (
stateObj.attributes.target_temp_low &&
stateObj.attributes.target_temp_high
) {
if (e.handle.index === 1) {
this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity,
target_temp_low: e.handle.value,
target_temp_high: stateObj.attributes.target_temp_high,
});
} else {
this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity,
target_temp_low: stateObj.attributes.target_temp_low,
target_temp_high: e.handle.value,
});
}
} else {
this.hass!.callService("climate", "set_temperature", {
entity_id: this._config!.entity,
temperature: e.value,
});
}
}
private _renderIcon(mode: string, currentMode: string): TemplateResult {
if (!modeIcons[mode]) {
return html``;
}
return html`
<ha-icon
class="${classMap({ "selected-icon": currentMode === mode })}"
.mode="${mode}"
.icon="${modeIcons[mode]}"
@click="${this._handleModeClick}"
></ha-icon>
`;
}
private _handleModeClick(e: MouseEvent): void {
this.hass!.callService("climate", "set_operation_mode", {
entity_id: this._config!.entity,
operation_mode: (e.currentTarget as any).mode,
});
}
}
declare global {

View File

@@ -2,7 +2,22 @@ import "../../../cards/ha-weather-card";
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
// should be interface when converted to TS
export const Config = {
entity: "",
name: "",
};
class HuiWeatherForecastCard extends LegacyWrapperCard {
static async getConfigElement() {
await import(/* webpackChunkName: "hui-weather-forecast-card-editor" */ "../editor/config-elements/hui-weather-forecast-card-editor");
return document.createElement("hui-weather-forecast-card-editor");
}
static getStubConfig() {
return {};
}
constructor() {
super("ha-weather-card", "weather");
}

View File

@@ -1,38 +0,0 @@
const EXCLUDED_DOMAINS = ["zone"];
function computeUsedEntities(config) {
const entities = new Set();
function addEntityId(entity) {
if (typeof entity === "string") {
entities.add(entity);
} else if (entity.entity) {
entities.add(entity.entity);
}
}
function addEntities(obj) {
if (obj.entity) addEntityId(obj.entity);
if (obj.entities) obj.entities.forEach((entity) => addEntityId(entity));
if (obj.card) addEntities(obj.card);
if (obj.cards) obj.cards.forEach((card) => addEntities(card));
if (obj.badges) obj.badges.forEach((badge) => addEntityId(badge));
}
config.views.forEach((view) => addEntities(view));
return entities;
}
export default function computeUnusedEntities(hass, config) {
const usedEntities = computeUsedEntities(config);
return Object.keys(hass.states)
.filter(
(entity) =>
!usedEntities.has(entity) &&
!(
config.excluded_entities && config.excluded_entities.includes(entity)
) &&
!EXCLUDED_DOMAINS.includes(entity.split(".", 1)[0])
)
.sort();
}

View File

@@ -0,0 +1,54 @@
import { LovelaceConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
const EXCLUDED_DOMAINS = ["zone"];
const computeUsedEntities = (config) => {
const entities = new Set();
const addEntityId = (entity) => {
if (typeof entity === "string") {
entities.add(entity);
} else if (entity.entity) {
entities.add(entity.entity);
}
};
const addEntities = (obj) => {
if (obj.entity) {
addEntityId(obj.entity);
}
if (obj.entities) {
obj.entities.forEach((entity) => addEntityId(entity));
}
if (obj.card) {
addEntities(obj.card);
}
if (obj.cards) {
obj.cards.forEach((card) => addEntities(card));
}
if (obj.badges) {
obj.badges.forEach((badge) => addEntityId(badge));
}
};
config.views.forEach((view) => addEntities(view));
return entities;
};
export const computeUnusedEntities = (
hass: HomeAssistant,
config: LovelaceConfig
): string[] => {
const usedEntities = computeUsedEntities(config);
return Object.keys(hass.states)
.filter(
(entity) =>
!usedEntities.has(entity) &&
!(
config.excluded_entities && config.excluded_entities.includes(entity)
) &&
!EXCLUDED_DOMAINS.includes(entity.split(".", 1)[0])
)
.sort();
};

View File

@@ -5,7 +5,11 @@ import "../cards/hui-conditional-card";
import "../cards/hui-entities-card";
import "../cards/hui-entity-button-card";
import "../cards/hui-entity-filter-card";
import "../cards/hui-error-card";
import {
createErrorCardElement,
createErrorCardConfig,
HuiErrorCard,
} from "../cards/hui-error-card";
import "../cards/hui-glance-card";
import "../cards/hui-history-graph-card";
import "../cards/hui-horizontal-stack-card";
@@ -25,8 +29,8 @@ import "../cards/hui-shopping-list-card";
import "../cards/hui-thermostat-card";
import "../cards/hui-weather-forecast-card";
import "../cards/hui-gauge-card";
import createErrorCardConfig from "./create-error-card-config";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
const CARD_TYPES = new Set([
"alarm-panel",
@@ -58,24 +62,29 @@ const CARD_TYPES = new Set([
const CUSTOM_TYPE_PREFIX = "custom:";
const TIMEOUT = 2000;
function _createElement(tag, config) {
const element = document.createElement(tag);
const _createElement = (
tag: string,
config: LovelaceCardConfig
): LovelaceCard | HuiErrorCard => {
const element = document.createElement(tag) as LovelaceCard;
try {
element.setConfig(config);
} catch (err) {
// eslint-disable-next-line
// tslint:disable-next-line
console.error(tag, err);
// eslint-disable-next-line
return _createErrorElement(err.message, config);
}
return element;
}
};
function _createErrorElement(error, config) {
return _createElement("hui-error-card", createErrorCardConfig(error, config));
}
const _createErrorElement = (
error: string,
config: LovelaceCardConfig
): HuiErrorCard => createErrorCardElement(createErrorCardConfig(error, config));
export default function createCardElement(config) {
export const createCardElement = (
config: LovelaceCardConfig
): LovelaceCard | HuiErrorCard => {
if (!config || typeof config !== "object" || !config.type) {
return _createErrorElement("No card type configured.", config);
}
@@ -111,4 +120,4 @@ export default function createCardElement(config) {
}
return _createElement(`hui-${config.type}-card`, config);
}
};

View File

@@ -1,7 +0,0 @@
export default function createErrorConfig(error, origConfig) {
return {
type: "error",
error,
origConfig,
};
}

View File

@@ -6,7 +6,12 @@ import "../elements/hui-state-icon-element";
import "../elements/hui-state-label-element";
import { fireEvent } from "../../../common/dom/fire_event";
import createErrorCardConfig from "./create-error-card-config";
import {
createErrorCardElement,
createErrorCardConfig,
HuiErrorCard,
} from "../cards/hui-error-card";
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
const CUSTOM_TYPE_PREFIX = "custom:";
const ELEMENT_TYPES = new Set([
@@ -19,22 +24,25 @@ const ELEMENT_TYPES = new Set([
]);
const TIMEOUT = 2000;
function _createElement(tag, config) {
const element = document.createElement(tag);
const _createElement = (
tag: string,
config: LovelaceElementConfig
): LovelaceElement | HuiErrorCard => {
const element = document.createElement(tag) as LovelaceElement;
try {
element.setConfig(config);
} catch (err) {
// eslint-disable-next-line
// tslint:disable-next-line
console.error(tag, err);
// eslint-disable-next-line
return _createErrorElement(err.message, config);
}
return element;
}
};
function _createErrorElement(error, config) {
return _createElement("hui-error-card", createErrorCardConfig(error, config));
}
const _createErrorElement = (
error: string,
config: LovelaceElementConfig
): HuiErrorCard => createErrorCardElement(createErrorCardConfig(error, config));
function _hideErrorElement(element) {
element.style.display = "None";
@@ -43,7 +51,9 @@ function _hideErrorElement(element) {
}, TIMEOUT);
}
export default function createHuiElement(config) {
export const createHuiElement = (
config: LovelaceElementConfig
): LovelaceElement | HuiErrorCard => {
if (!config || typeof config !== "object" || !config.type) {
return _createErrorElement("No element type configured.", config);
}
@@ -76,4 +86,4 @@ export default function createHuiElement(config) {
}
return _createElement(`hui-${config.type}-element`, config);
}
};

View File

@@ -1,5 +1,10 @@
import { fireEvent } from "../../../common/dom/fire_event";
import {
createErrorCardElement,
createErrorCardConfig,
HuiErrorCard,
} from "../cards/hui-error-card";
import "../entity-rows/hui-climate-entity-row";
import "../entity-rows/hui-cover-entity-row";
import "../entity-rows/hui-group-entity-row";
@@ -18,8 +23,7 @@ import "../special-rows/hui-call-service-row";
import "../special-rows/hui-divider-row";
import "../special-rows/hui-section-row";
import "../special-rows/hui-weblink-row";
import createErrorCardConfig from "./create-error-card-config";
import { EntityConfig, EntityRow } from "../entity-rows/types";
const CUSTOM_TYPE_PREFIX = "custom:";
const SPECIAL_TYPES = new Set([
@@ -51,32 +55,37 @@ const DOMAIN_TO_ELEMENT_TYPE = {
};
const TIMEOUT = 2000;
function _createElement(tag, config) {
const element = document.createElement(tag);
const _createElement = (
tag: string,
config: EntityConfig
): EntityRow | HuiErrorCard => {
const element = document.createElement(tag) as EntityRow;
try {
if ("setConfig" in element) element.setConfig(config);
element.setConfig(config);
} catch (err) {
// eslint-disable-next-line
// tslint:disable-next-line
console.error(tag, err);
// eslint-disable-next-line
return _createErrorElement(err.message, config);
}
return element;
}
};
function _createErrorElement(error, config) {
return _createElement("hui-error-card", createErrorCardConfig(error, config));
}
const _createErrorElement = (
error: string,
config: EntityConfig
): HuiErrorCard => createErrorCardElement(createErrorCardConfig(error, config));
function _hideErrorElement(element) {
const _hideErrorElement = (element) => {
element.style.display = "None";
return window.setTimeout(() => {
element.style.display = "";
}, TIMEOUT);
}
};
export default function createRowElement(config) {
export const createRowElement = (
config: EntityConfig
): EntityRow | HuiErrorCard => {
let tag;
if (
@@ -116,4 +125,4 @@ export default function createRowElement(config) {
tag = `hui-${DOMAIN_TO_ELEMENT_TYPE[domain] || "text"}-entity-row`;
return _createElement(tag, config);
}
};

View File

@@ -110,7 +110,8 @@ class LongPress extends HTMLElement implements LongPress {
const clickEnd = (ev: Event) => {
if (
this.cooldownEnd ||
(ev instanceof TouchEvent && this.timer === undefined)
(["touchend", "touchcancel"].includes(ev.type) &&
this.timer === undefined)
) {
return;
}

View File

@@ -42,6 +42,11 @@ const computeCards = (
type: "alarm-panel",
entity: entityId,
});
} else if (domain === "camera") {
cards.push({
type: "picture-entity",
entity: entityId,
});
} else if (domain === "climate") {
cards.push({
type: "thermostat",
@@ -94,7 +99,7 @@ const computeDefaultViewStates = (hass: HomeAssistant): HassEntities => {
const generateViewConfig = (
localize: LocalizeFunc,
id: string,
path: string,
title: string | undefined,
icon: string | undefined,
entities: HassEntities,
@@ -158,7 +163,7 @@ const generateViewConfig = (
});
return {
id,
path,
title,
icon,
badges,
@@ -228,7 +233,6 @@ export const generateLovelaceConfig = (
}
return {
_frontendAuto: true,
title,
views,
};

View File

@@ -0,0 +1,132 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "../../../components/ha-service-picker";
import { HomeAssistant } from "../../../types";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { EditorTarget } from "../editor/types";
import {
ActionConfig,
NavigateActionConfig,
CallServiceActionConfig,
} from "../../../data/lovelace";
declare global {
// for fire event
interface HASSDomEvents {
"action-changed": undefined;
}
// for add event listener
interface HTMLElementEventMap {
"action-changed": HASSDomEvent<undefined>;
}
}
export class HuiActionEditor extends LitElement {
public config?: ActionConfig;
public label?: string;
public actions?: string[];
protected hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return { hass: {}, config: {}, label: {}, actions: {} };
}
get _action(): string {
return this.config!.action || "";
}
get _navigation_path(): string {
const config = this.config! as NavigateActionConfig;
return config.navigation_path || "";
}
get _service(): string {
const config = this.config! as CallServiceActionConfig;
return config.service || "";
}
protected render(): TemplateResult {
if (!this.hass || !this.actions) {
return html``;
}
return html`
<paper-dropdown-menu
.label="${this.label}"
.configValue="${"action"}"
@value-changed="${this._valueChanged}"
>
<paper-listbox
slot="dropdown-content"
.selected="${this.actions.indexOf(this._action)}"
>
${
this.actions.map((action) => {
return html`
<paper-item>${action}</paper-item>
`;
})
}
</paper-listbox>
</paper-dropdown-menu>
${
this._action === "navigate"
? html`
<paper-input
label="Navigation Path"
.value="${this._navigation_path}"
.configValue="${"navigation_path"}"
@value-changed="${this._valueChanged}"
></paper-input>
`
: ""
}
${
this.config && this.config.action === "call-service"
? html`
<ha-service-picker
.hass="${this.hass}"
.value="${this._service}"
.configValue="${"service"}"
@value-changed="${this._valueChanged}"
></ha-service-picker>
<h3>Toggle Editor to input Service Data</h3>
`
: ""
}
`;
}
private _valueChanged(ev: Event): void {
if (!this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (
this.config &&
this.config[this[`${target.configValue}`]] === target.value
) {
return;
}
if (target.configValue === "action") {
this.config = { action: "none" };
}
if (target.configValue) {
this.config = { ...this.config!, [target.configValue!]: target.value };
fireEvent(this, "action-changed");
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-action-editor": HuiActionEditor;
}
}
customElements.define("hui-action-editor", HuiActionEditor);

View File

@@ -1,90 +1,155 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "@polymer/paper-button/paper-button";
import { fireEvent } from "../../../common/dom/fire_event";
import { showEditCardDialog } from "../editor/show-edit-card-dialog";
import "@polymer/paper-menu-button/paper-menu-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-listbox/paper-listbox";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { confDeleteCard } from "../editor/delete-card";
import { HomeAssistant } from "../../../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
declare global {
// for fire event
interface HASSDomEvents {
"show-edit-card": {
cardConfig?: LovelaceCardConfig;
viewId?: string | number;
add: boolean;
reloadLovelace: () => void;
};
}
}
import { Lovelace } from "../types";
import { swapCard } from "../editor/config-util";
import { showMoveCardViewDialog } from "../editor/card-editor/show-move-card-view-dialog";
export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) {
public cardConfig?: LovelaceCardConfig;
protected hass?: HomeAssistant;
public hass?: HomeAssistant;
public lovelace?: Lovelace;
public path?: [number, number];
static get properties(): PropertyDeclarations {
return { hass: {} };
return { hass: {}, lovelace: {}, path: {} };
}
protected render() {
return html`
<style>
div {
div.options {
border-top: 1px solid #e8e8e8;
padding: 5px 16px;
padding: 5px 8px;
background: var(--paper-card-background-color, white);
box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px,
rgba(0, 0, 0, 0.12) 0px 1px 5px 0px,
rgba(0, 0, 0, 0.12) 0px 1px 5px -4px,
rgba(0, 0, 0, 0.2) 0px 3px 1px -2px;
display: flex;
}
div.options .primary-actions {
flex: 1;
margin: auto;
}
div.options .secondary-actions {
flex: 4;
text-align: right;
}
paper-button {
color: var(--primary-color);
font-weight: 500;
letter-spacing: 0.05em;
font-size: 16px;
padding: 0;
margin: 0;
}
paper-button.warning:not([disabled]) {
color: var(--google-red-500);
paper-icon-button {
color: var(--primary-text-color);
}
paper-icon-button.move-arrow[disabled] {
color: var(--disabled-text-color);
}
paper-menu-button {
color: var(--secondary-text-color);
padding: 0;
}
paper-item.header {
color: var(--primary-text-color);
text-transform: uppercase;
font-weight: 500;
font-size: 14px;
}
paper-item {
cursor: pointer;
}
</style>
<slot></slot>
<div>
<paper-button @click="${this._editCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.edit")
}</paper-button
>
<paper-button class="warning" @click="${this._deleteCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.delete")
}</paper-button
>
<div class="options">
<div class="primary-actions">
<paper-button @click="${this._editCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.edit")
}</paper-button
>
</div>
<div class="secondary-actions">
<paper-icon-button
title="Move card down"
class="move-arrow"
icon="hass:arrow-down"
@click="${this._cardDown}"
?disabled="${
this.lovelace!.config.views[this.path![0]].cards!.length ===
this.path![1] + 1
}"
></paper-icon-button>
<paper-icon-button
title="Move card up"
class="move-arrow"
icon="hass:arrow-up"
@click="${this._cardUp}"
?disabled="${this.path![1] === 0}"
></paper-icon-button>
<paper-menu-button>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item @click="${this._moveCard}">Move Card</paper-item>
<paper-item @click="${this._deleteCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.delete")
}</paper-item
>
</paper-listbox>
</paper-menu-button>
</div>
</div>
`;
}
private _editCard(): void {
if (!this.cardConfig) {
return;
}
showEditCardDialog(this, {
cardConfig: this.cardConfig,
add: false,
reloadLovelace: () => fireEvent(this, "config-refresh"),
lovelace: this.lovelace!,
path: this.path!,
});
}
private _deleteCard(): void {
if (!this.cardConfig) {
return;
}
if (!this.cardConfig.id) {
this._editCard();
return;
}
confDeleteCard(this.hass!, this.cardConfig.id, () =>
fireEvent(this, "config-refresh")
private _cardUp(): void {
const lovelace = this.lovelace!;
const path = this.path!;
lovelace.saveConfig(
swapCard(lovelace.config, path, [path[0], path[1] - 1])
);
}
private _cardDown(): void {
const lovelace = this.lovelace!;
const path = this.path!;
lovelace.saveConfig(
swapCard(lovelace.config, path, [path[0], path[1] + 1])
);
}
private _moveCard(): void {
showMoveCardViewDialog(this, {
path: this.path!,
lovelace: this.lovelace!,
});
}
private _deleteCard(): void {
confDeleteCard(this.lovelace!, this.path!);
}
}
declare global {

View File

@@ -25,6 +25,7 @@ class HuiEntitiesToggle extends LitElement {
}
public updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("entities")) {
this._toggleEntities = this.entities!.filter(
(entityId) =>

View File

@@ -19,7 +19,7 @@ declare global {
export class HuiThemeSelectionEditor extends hassLocalizeLitMixin(LitElement) {
public value?: string;
protected hass?: HomeAssistant;
public hass?: HomeAssistant;
static get properties(): PropertyDeclarations {
return {

View File

@@ -78,6 +78,7 @@ class HuiTimestampDisplay extends hassLocalizeLitMixin(LitElement) {
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (!changedProperties.has("format") || !this._connected) {
return;
}

View File

@@ -46,7 +46,7 @@ export class HuiNotificationsButton extends EventsMixin(PolymerElement) {
static get properties() {
return {
notificationsOpen: {
open: {
type: Boolean,
notify: true,
},
@@ -58,7 +58,7 @@ export class HuiNotificationsButton extends EventsMixin(PolymerElement) {
}
_clicked() {
this.notificationsOpen = true;
this.open = true;
}
_hasNotifications(notifications) {

View File

@@ -2,22 +2,11 @@ import { html, LitElement } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-button/paper-button";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { getCardElementTag } from "../common/get-card-element-tag";
import { CardPickTarget } from "./types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { uid } from "../../../common/util/uid";
declare global {
interface HASSDomEvents {
"card-picked": {
config: LovelaceCardConfig;
};
}
}
import { HomeAssistant } from "../../../../types";
import { LovelaceCardConfig } from "../../../../data/lovelace";
import { getCardElementTag } from "../../common/get-card-element-tag";
import { CardPickTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
const cards = [
{ name: "Alarm panel", type: "alarm-panel" },
@@ -28,7 +17,7 @@ const cards = [
{ name: "Gauge", type: "gauge" },
{ name: "Glance", type: "glance" },
{ name: "History Graph", type: "history-graph" },
{ name: "Horizontal Stack", type: "horizontal-graph" },
{ name: "Horizontal Stack", type: "horizontal-stack" },
{ name: "iFrame", type: "iframe" },
{ name: "Light", type: "light" },
{ name: "Map", type: "map" },
@@ -47,7 +36,8 @@ const cards = [
];
export class HuiCardPicker extends hassLocalizeLitMixin(LitElement) {
protected hass?: HomeAssistant;
public hass?: HomeAssistant;
public cardPicked?: (cardConf: LovelaceCardConfig) => void;
protected render(): TemplateResult {
return html`
@@ -90,16 +80,14 @@ export class HuiCardPicker extends hassLocalizeLitMixin(LitElement) {
const tag = getCardElementTag(type);
const elClass = customElements.get(tag);
let config: LovelaceCardConfig = { type, id: uid() };
let config: LovelaceCardConfig = { type };
if (elClass && elClass.getStubConfig) {
const cardConfig = elClass.getStubConfig(this.hass);
config = { ...config, ...cardConfig };
}
fireEvent(this, "card-picked", {
config,
});
this.cardPicked!(config);
}
}

View File

@@ -1,12 +1,12 @@
import "@polymer/paper-input/paper-textarea";
import createCardElement from "../common/create-card-element";
import createErrorCardConfig from "../common/create-error-card-config";
import { HomeAssistant } from "../../../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { LovelaceCard } from "../types";
import { ConfigError } from "./types";
import { getCardElementTag } from "../common/get-card-element-tag";
import { createCardElement } from "../../common/create-card-element";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardConfig } from "../../../../data/lovelace";
import { LovelaceCard } from "../../types";
import { ConfigError } from "../types";
import { getCardElementTag } from "../../common/get-card-element-tag";
import { createErrorCardConfig } from "../../cards/hui-error-card";
export class HuiCardPreview extends HTMLElement {
private _hass?: HomeAssistant;

View File

@@ -0,0 +1,93 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../../types";
import { HASSDomEvent } from "../../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../../data/lovelace";
import "./hui-edit-card";
import "./hui-dialog-pick-card";
import { EditCardDialogParams } from "./show-edit-card-dialog";
declare global {
// for fire event
interface HASSDomEvents {
"reload-lovelace": undefined;
}
// for add event listener
interface HTMLElementEventMap {
"reload-lovelace": HASSDomEvent<undefined>;
}
}
export class HuiDialogEditCard extends LitElement {
protected hass?: HomeAssistant;
private _params?: EditCardDialogParams;
private _cardConfig?: LovelaceCardConfig;
static get properties(): PropertyDeclarations {
return {
hass: {},
_params: {},
_cardConfig: {},
};
}
constructor() {
super();
this._cardPicked = this._cardPicked.bind(this);
this._cancel = this._cancel.bind(this);
}
public async showDialog(params: EditCardDialogParams): Promise<void> {
this._params = params;
this._cardConfig =
params.path.length === 2
? (this._cardConfig = params.lovelace.config.views[
params.path[0]
].cards![params.path[1]])
: undefined;
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
if (!this._cardConfig) {
// Card picker
return html`
<hui-dialog-pick-card
.hass="${this.hass}"
.cardPicked="${this._cardPicked}"
.closeDialog="${this._cancel}"
></hui-dialog-pick-card>
`;
}
return html`
<hui-edit-card
.hass="${this.hass}"
.lovelace="${this._params.lovelace}"
.path="${this._params.path}"
.cardConfig="${this._cardConfig}"
.closeDialog="${this._cancel}"
>
</hui-edit-card>
`;
}
private _cardPicked(cardConf: LovelaceCardConfig) {
this._cardConfig = cardConf;
}
private _cancel() {
this._params = undefined;
this._cardConfig = undefined;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-dialog-edit-card": HuiDialogEditCard;
}
}
customElements.define("hui-dialog-edit-card", HuiDialogEditCard);

View File

@@ -0,0 +1,106 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-item/paper-item";
// tslint:disable-next-line:no-duplicate-imports
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { moveCard } from "../config-util";
import { MoveCardViewDialogParams } from "./show-move-card-view-dialog";
export class HuiDialogMoveCardView extends hassLocalizeLitMixin(LitElement) {
private _params?: MoveCardViewDialogParams;
static get properties(): PropertyDeclarations {
return {
_params: {},
};
}
public async showDialog(params: MoveCardViewDialogParams): Promise<void> {
this._params = params;
await this.updateComplete;
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
return html`
<style>
paper-item {
margin: 8px;
cursor: pointer;
}
paper-item[active] {
color: var(--primary-color);
}
paper-item[active]:before {
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: "";
background-color: var(--primary-color);
opacity: 0.12;
transition: opacity 15ms linear;
will-change: opacity;
}
</style>
<paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
>
<h2>Choose view to move card</h2>
${
this._params!.lovelace!.config.views.map((view, index) => {
return html`
<paper-item
?active="${this._params!.path![0] === index}"
@click="${this._moveCard}"
.index="${index}"
>${view.title}</paper-item
>
`;
})
}
</paper-dialog>
`;
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
}
private _moveCard(e: Event): void {
const newView = (e.currentTarget! as any).index;
const path = this._params!.path!;
if (newView === path[0]) {
return;
}
const lovelace = this._params!.lovelace!;
lovelace.saveConfig(moveCard(lovelace.config, path, [newView!]));
this._dialog.close();
}
private _openedChanged(ev: MouseEvent) {
if (!(ev.detail as any).value) {
this._params = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-dialog-move-card-view": HuiDialogMoveCardView;
}
}
customElements.define("hui-dialog-move-card-view", HuiDialogMoveCardView);

View File

@@ -0,0 +1,58 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "./hui-card-picker";
import { HomeAssistant } from "../../../../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { LovelaceCardConfig } from "../../../../data/lovelace";
export class HuiDialogPickCard extends hassLocalizeLitMixin(LitElement) {
public hass?: HomeAssistant;
public cardPicked?: (cardConf: LovelaceCardConfig) => void;
public closeDialog?: () => void;
static get properties(): PropertyDeclarations {
return {};
}
protected render(): TemplateResult {
return html`
<paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
>
<h2>${this.localize("ui.panel.lovelace.editor.edit_card.header")}</h2>
<paper-dialog-scrollable>
<hui-card-picker
.hass="${this.hass}"
.cardPicked="${this.cardPicked}"
></hui-card-picker>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<paper-button @click="${this._skipPick}">SKIP</paper-button>
</div>
</paper-dialog>
`;
}
private _openedChanged(ev) {
if (!ev.detail.value) {
this.closeDialog!();
}
}
private _skipPick() {
this.cardPicked!({ type: "" });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-dialog-pick-card": HuiDialogPickCard;
}
}
customElements.define("hui-dialog-pick-card", HuiDialogPickCard);

View File

@@ -15,31 +15,22 @@ import "@polymer/paper-dialog/paper-dialog";
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import { HomeAssistant } from "../../../types";
import {
addCard,
updateCardConfig,
LovelaceCardConfig,
} from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardConfig } from "../../../../data/lovelace";
import { fireEvent } from "../../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import "./hui-yaml-editor";
import "./hui-card-picker";
import "./hui-card-preview";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { HuiCardPreview } from "./hui-card-preview";
import { LovelaceCardEditor } from "../types";
import {
YamlChangedEvent,
CardPickedEvent,
ConfigValue,
ConfigError,
} from "./types";
import { extYamlSchema } from "./yaml-ext-schema";
import { EntityConfig } from "../entity-rows/types";
import { getCardElementTag } from "../common/get-card-element-tag";
import { LovelaceCardEditor, Lovelace } from "../../types";
import { YamlChangedEvent, ConfigValue, ConfigError } from "../types";
import { extYamlSchema } from "../yaml-ext-schema";
import { EntityConfig } from "../../entity-rows/types";
import { getCardElementTag } from "../../common/get-card-element-tag";
import { addCard, replaceCard } from "../config-util";
declare global {
interface HASSDomEvents {
@@ -52,17 +43,30 @@ declare global {
"config-changed": {
config: LovelaceCardConfig;
};
"cancel-edit-card": {};
}
}
export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
public hass?: HomeAssistant;
public lovelace?: Lovelace;
public path?: [number] | [number, number];
public cardConfig?: LovelaceCardConfig;
public closeDialog?: () => void;
private _configElement?: LovelaceCardEditor | null;
private _uiEditor?: boolean;
private _configValue?: ConfigValue;
private _configState?: string;
private _loading?: boolean;
private _saving: boolean;
private _errorMsg?: TemplateResult;
private _cardType?: string;
static get properties(): PropertyDeclarations {
return {
hass: {},
cardConfig: {},
viewId: {},
_cardId: {},
viewIndex: {},
_cardIndex: {},
_configElement: {},
_configValue: {},
_configState: {},
@@ -81,38 +85,15 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
return this.shadowRoot!.querySelector("hui-card-preview")!;
}
public cardConfig?: LovelaceCardConfig;
public viewId?: string | number;
protected hass?: HomeAssistant;
private _cardId?: string;
private _configElement?: LovelaceCardEditor | null;
private _uiEditor?: boolean;
private _configValue?: ConfigValue;
private _configState?: string;
private _loading?: boolean;
private _saving: boolean;
private _errorMsg?: TemplateResult;
private _cardType?: string;
protected constructor() {
super();
this._saving = false;
}
public async showDialog(): Promise<void> {
// Wait till dialog is rendered.
if (this._dialog == null) {
await this.updateComplete;
}
this._dialog.open();
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
!changedProperties.has("cardConfig") &&
!changedProperties.has("viewId")
) {
if (!changedProperties.has("cardConfig")) {
return;
}
@@ -122,17 +103,8 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
this._errorMsg = undefined;
this._configElement = undefined;
if (this.cardConfig && String(this.cardConfig.id) !== this._cardId) {
this._loading = true;
this._cardId = String(this.cardConfig.id);
this._loadConfigElement(this.cardConfig);
} else {
this._cardId = undefined;
}
if (this.viewId && !this.cardConfig) {
this._resizeDialog();
}
this._loading = true;
this._loadConfigElement(this.cardConfig!);
}
protected render(): TemplateResult {
@@ -147,7 +119,6 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
content = html`
<hui-yaml-editor
.hass="${this.hass}"
.cardId="${this._cardId}"
.yaml="${this._configValue!.value}"
@yaml-changed="${this._handleYamlChanged}"
></hui-yaml-editor>
@@ -157,18 +128,15 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
<hr />
<hui-card-preview .hass="${this.hass}"> </hui-card-preview>
`;
} else if (this.viewId && !this.cardConfig) {
content = html`
<hui-card-picker
.hass="${this.hass}"
@card-picked="${this._handleCardPicked}"
></hui-card-picker>
`;
}
return html`
${this.renderStyle()}
<paper-dialog with-backdrop>
<paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
>
<h2>${this.localize("ui.panel.lovelace.editor.edit_card.header")}</h2>
<paper-spinner
?active="${this._loading}"
@@ -192,6 +160,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
? html`
<div class="paper-dialog-buttons">
<paper-button
class="toggle-button"
?hidden="${!this._configValue || !this._configValue.value}"
?disabled="${
this._configElement === null || this._configState !== "OK"
@@ -203,7 +172,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
)
}</paper-button
>
<paper-button @click="${this._closeDialog}"
<paper-button @click="${this.closeDialog}"
>${this.localize("ui.common.cancel")}</paper-button
>
<paper-button
@@ -268,19 +237,13 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
margin-bottom: 4px;
display: block;
}
.toggle-button {
margin-right: auto;
}
</style>
`;
}
private _save(): void {
this._saving = true;
this._updateConfigInBackend();
}
private _saveDone(): void {
this._saving = false;
}
private async _loadedDialog(): Promise<void> {
await this.updateComplete;
this._loading = false;
@@ -292,58 +255,42 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
fireEvent(this._dialog, "iron-resize");
}
private _closeDialog(): void {
this.cardConfig = undefined;
this.viewId = undefined;
fireEvent(this, "cancel-edit-card");
this._dialog.close();
}
private async _updateConfigInBackend(): Promise<void> {
private async _save(): Promise<void> {
if (!this._isConfigValid()) {
alert("Your config is not valid, please fix your config before saving.");
this._saveDone();
return;
}
if (!this._isConfigChanged()) {
this._closeDialog();
this._saveDone();
this.closeDialog!();
return;
}
this._saving = true;
const cardConf: LovelaceCardConfig =
this._configValue!.format === "yaml"
? yaml.safeLoad(this._configValue!.value!, {
schema: extYamlSchema,
})
: this._configValue!.value!;
try {
if (this.viewId) {
await addCard(
this.hass!,
String(this.viewId),
this._configValue!.value!,
this._configValue!.format
);
} else {
await updateCardConfig(
this.hass!,
this._cardId!,
this._configValue!.value!,
this._configValue!.format
);
}
fireEvent(this, "reload-lovelace");
this._closeDialog();
this._saveDone();
const lovelace = this.lovelace!;
await lovelace.saveConfig(
this._creatingCard
? addCard(lovelace.config, this.path as [number], cardConf)
: replaceCard(
lovelace.config,
this.path as [number, number],
cardConf
)
);
this.closeDialog!();
} catch (err) {
alert(`Saving failed: ${err.message}`);
this._saveDone();
}
}
private async _handleCardPicked(ev: CardPickedEvent): Promise<void> {
const succes = await this._loadConfigElement(ev.detail.config);
if (!succes) {
this._configValue = {
format: "yaml",
value: yaml.safeDump(ev.detail.config),
};
} finally {
this._saving = false;
}
}
@@ -394,14 +341,10 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
private async _toggleEditor(): Promise<void> {
if (this._uiEditor && this._configValue!.format === "json") {
if (this._isConfigChanged()) {
this._configValue = {
format: "yaml",
value: yaml.safeDump(this._configValue!.value),
};
} else {
this._configValue = { format: "yaml", value: undefined };
}
this._configValue = {
format: "yaml",
value: yaml.safeDump(this._configValue!.value),
};
this._uiEditor = !this._uiEditor;
} else if (this._configElement && this._configValue!.format === "yaml") {
const yamlConfig = this._configValue!.value;
@@ -438,12 +381,12 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
}
private _isConfigChanged(): boolean {
if (this.viewId) {
if (this._creatingCard) {
return true;
}
const configValue =
this._configValue!.format === "yaml"
? yaml.safeDump(this._configValue!.value)
? yaml.safeLoad(this._configValue!.value)
: this._configValue!.value;
return JSON.stringify(configValue) !== JSON.stringify(this.cardConfig);
}
@@ -465,6 +408,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
if (elClass && elClass.getConfigElement) {
configElement = await elClass.getConfigElement();
} else {
this._configValue = { format: "yaml", value: yaml.safeDump(conf) };
this._uiEditor = false;
this._configElement = null;
return false;
@@ -477,6 +421,10 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
Your config is not supported by the UI editor:<br /><b>${err.message}</b
><br />Falling back to YAML editor.
`;
this._configValue = {
format: "yaml",
value: yaml.safeDump(conf),
};
this._uiEditor = false;
this._configElement = null;
return false;
@@ -492,6 +440,16 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
this._updatePreview(conf);
return true;
}
private get _creatingCard(): boolean {
return this.path!.length === 1;
}
private _openedChanged(ev) {
if (!ev.detail.value) {
this.closeDialog!();
}
}
}
declare global {

View File

@@ -1,42 +1,29 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-spinner/paper-spinner";
import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import { getCardConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
export class HuiYAMLEditor extends LitElement {
public cardId?: string;
protected hass?: HomeAssistant;
private _yaml?: string;
private _loading?: boolean;
static get properties(): PropertyDeclarations {
return { _yaml: {}, cardId: {} };
return { _yaml: {} };
}
set yaml(yaml: string) {
if (yaml === undefined) {
this._loading = true;
this._loadConfig();
return;
} else {
this._yaml = yaml;
if (this._loading) {
this._loading = false;
}
}
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<paper-spinner
?active="${this._loading}"
alt="Loading"
class="center"
></paper-spinner>
<paper-textarea
max-rows="10"
.value="${this._yaml}"
@@ -51,31 +38,10 @@ export class HuiYAMLEditor extends LitElement {
paper-textarea {
--paper-input-container-shared-input-style_-_font-family: monospace;
}
.center {
margin-left: auto;
margin-right: auto;
}
paper-spinner {
display: none;
}
paper-spinner[active] {
display: block;
}
</style>
`;
}
private async _loadConfig(): Promise<void> {
if (!this.hass || !this.cardId) {
return;
}
this._yaml = await getCardConfig(this.hass, this.cardId);
if (this._loading) {
this._loading = false;
}
}
private _valueChanged(ev: Event): void {
const target = ev.target! as any;
this._yaml = target.value;

View File

@@ -1,5 +1,5 @@
import { LovelaceCardConfig } from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Lovelace } from "../../types";
declare global {
// for fire event
@@ -13,17 +13,16 @@ const dialogShowEvent = "show-edit-card";
const dialogTag = "hui-dialog-edit-card";
export interface EditCardDialogParams {
cardConfig?: LovelaceCardConfig;
viewId?: string | number;
add: boolean;
reloadLovelace: () => void;
lovelace: Lovelace;
path: [number] | [number, number];
}
const registerEditCardDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-edit-card"),
dialogImport: () =>
import(/* webpackChunkName: "hui-dialog-edit-card" */ "./hui-dialog-edit-card"),
});
export const showEditCardDialog = (

View File

@@ -0,0 +1,35 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import { Lovelace } from "../../types";
declare global {
// for fire event
interface HASSDomEvents {
"show-move-card-view": MoveCardViewDialogParams;
}
}
let registeredDialog = false;
export interface MoveCardViewDialogParams {
path: [number, number];
lovelace: Lovelace;
}
const registerEditCardDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent: "show-move-card-view",
dialogTag: "hui-dialog-move-card-view",
dialogImport: () =>
import(/* webpackChunkName: "hui-dialog-move-card-view" */ "./hui-dialog-move-card-view"),
});
export const showMoveCardViewDialog = (
element: HTMLElement,
moveCardViewDialogParams: MoveCardViewDialogParams
) => {
if (!registeredDialog) {
registeredDialog = true;
registerEditCardDialog(element);
}
fireEvent(element, "show-move-card-view", moveCardViewDialogParams);
};

View File

@@ -0,0 +1,195 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-alarm-panel-card";
import { configElementStyle } from "./config-elements-style";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-icon";
const cardConfigStruct = struct({
type: "string",
entity: "string?",
name: "string?",
states: "array?",
});
export class HuiAlarmPanelCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
get _states(): string[] {
return this._config!.states || [];
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const states = ["arm_home", "arm_away", "arm_night", "arm_custom_bypass"];
return html`
${configElementStyle} ${this.renderStyle()}
<div class="card-config">
<div class="side-by-side">
<paper-input
label="Name"
.value="${this._name}"
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="alarm_control_panel"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
</div>
<span>Used States</span> ${
this._states.map((state, index) => {
return html`
<div class="states">
<paper-item>${state}</paper-item>
<ha-icon
class="deleteState"
.value="${index}"
icon="hass:close"
@click=${this._stateRemoved}
></ha-icon>
</div>
`;
})
}
<paper-dropdown-menu
label="Available States"
@value-changed="${this._stateAdded}"
>
<paper-listbox slot="dropdown-content">
${
states.map((state) => {
return html`
<paper-item>${state}</paper-item>
`;
})
}
</paper-listbox>
</paper-dropdown-menu>
</div>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
.states {
display: flex;
flex-direction: row;
}
.deleteState {
visibility: hidden;
}
.states:hover > .deleteState {
visibility: visible;
}
ha-icon {
padding-top: 12px;
}
</style>
`;
}
private _stateRemoved(ev: EntitiesEditorEvent): void {
if (!this._config || !this._states || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
const index = Number(target.value);
if (index > -1) {
const newStates = this._states;
newStates.splice(index, 1);
this._config = {
...this._config,
states: newStates,
};
fireEvent(this, "config-changed", { config: this._config });
}
}
private _stateAdded(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (!target.value || this._states.indexOf(target.value) >= 0) {
return;
}
const newStates = this._states;
newStates.push(target.value);
this._config = {
...this._config,
states: newStates,
};
target.value = "";
fireEvent(this, "config-changed", { config: this._config });
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-alarm-panel-card-editor": HuiAlarmPanelCardEditor;
}
}
customElements.define("hui-alarm-panel-card-editor", HuiAlarmPanelCardEditor);

View File

@@ -1,13 +1,12 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { struct } from "../../common/structs/struct";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { processEditorEntities } from "../process-editor-entities";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
@@ -33,7 +32,6 @@ const entitiesConfigStruct = struct.union([
const cardConfigStruct = struct({
type: "string",
id: "string|number",
title: "string|number?",
theme: "string?",
show_header_toggle: "boolean?",
@@ -60,8 +58,7 @@ export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement)
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = { type: "entities", ...config };
this._config = config;
this._configEntities = processEditorEntities(config.entities);
}
@@ -75,7 +72,7 @@ export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement)
<div class="card-config">
<paper-input
label="Title"
value="${this._title}"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
@@ -118,11 +115,15 @@ export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement)
this._config.entities = ev.detail.entities;
this._configEntities = processEditorEntities(this._config.entities);
} else if (target.configValue) {
this._config = {
...this._config,
[target.configValue]:
target.checked !== undefined ? target.checked : target.value,
};
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue]:
target.checked !== undefined ? target.checked : target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });

View File

@@ -0,0 +1,165 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import {
EntitiesEditorEvent,
EditorTarget,
actionConfigStruct,
} from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-entity-button-card";
import { configElementStyle } from "./config-elements-style";
import { ActionConfig } from "../../../../data/lovelace";
import "../../components/hui-action-editor";
import "../../components/hui-theme-select-editor";
import "../../components/hui-entity-editor";
const cardConfigStruct = struct({
type: "string",
entity: "string?",
name: "string?",
icon: "string?",
tap_action: actionConfigStruct,
hold_action: actionConfigStruct,
theme: "string?",
});
export class HuiEntityButtonCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
get _icon(): string {
return this._config!.icon || "";
}
get _tap_action(): ActionConfig {
return this._config!.tap_action || { action: "more-info" };
}
get _hold_action(): ActionConfig {
return this._config!.hold_action || { action: "none" };
}
get _theme(): string {
return this._config!.theme || "default";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const actions = ["more-info", "toggle", "navigate", "call-service", "none"];
return html`
${configElementStyle}
<div class="card-config">
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
<div class="side-by-side">
<paper-input
label="Name (Optional)"
.value="${this._name}"
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Icon (Optional)"
.value="${this._icon}"
.configValue="${"icon"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
<div class="side-by-side">
<hui-action-editor
label="Tap Action"
.hass="${this.hass}"
.config="${this._tap_action}"
.actions="${actions}"
.configValue="${"tap_action"}"
@action-changed="${this._valueChanged}"
></hui-action-editor>
<hui-action-editor
label="Hold Action"
.hass="${this.hass}"
.config="${this._hold_action}"
.actions="${actions}"
.configValue="${"hold_action"}"
@action-changed="${this._valueChanged}"
></hui-action-editor>
</div>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (
this[`_${target.configValue}`] === target.value ||
this[`_${target.configValue}`] === target.config
) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value ? target.value : target.config,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-entity-button-card-editor": HuiEntityButtonCardEditor;
}
}
customElements.define(
"hui-entity-button-card-editor",
HuiEntityButtonCardEditor
);

View File

@@ -0,0 +1,244 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config, SeverityConfig } from "../../cards/hui-gauge-card";
import { configElementStyle } from "./config-elements-style";
import "../../components/hui-theme-select-editor";
import "../../components/hui-entity-editor";
const cardConfigStruct = struct({
type: "string",
name: "string?",
entity: "string?",
unit: "string?",
min: "number?",
max: "number?",
severity: "object?",
theme: "string?",
});
export class HuiGaugeCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
private _useSeverity?: boolean;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._useSeverity = config.severity ? true : false;
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _name(): string {
return this._config!.name || "";
}
get _entity(): string {
return this._config!.entity || "";
}
get _unit(): string {
return this._config!.unit || "";
}
get _theme(): string {
return this._config!.theme || "default";
}
get _min(): number {
return this._config!.number || 0;
}
get _max(): number {
return this._config!.max || 100;
}
get _severity(): SeverityConfig | undefined {
return this._config!.severity || undefined;
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle} ${this.renderStyle()}
<div class="card-config">
<div class="side-by-side">
<paper-input
label="Name"
.value="${this._name}"
.configValue=${"name"}
@value-changed="${this._valueChanged}"
></paper-input>
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="sensor"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
</div>
<div class="side-by-side">
<paper-input
label="Unit"
.value="${this._unit}"
.configValue=${"unit"}
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
<div class="side-by-side">
<paper-input
type="number"
label="Minimum"
.value="${this._min}"
.configValue=${"min"}
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
type="number"
label="Maximum"
.value="${this._max}"
.configValue=${"max"}
@value-changed="${this._valueChanged}"
></paper-input>
</div>
<div class="side-by-side">
<paper-toggle-button
?checked="${this._useSeverity !== false}"
@change="${this._toggleSeverity}"
>Define Severity?</paper-toggle-button
>
<div class="severity">
<paper-input
type="number"
label="Green"
.value="${this._severity ? this._severity.green : 0}"
.configValue=${"green"}
@value-changed="${this._severityChanged}"
></paper-input>
<paper-input
type="number"
label="Yellow"
.value="${this._severity ? this._severity.yellow : 0}"
.configValue=${"yellow"}
@value-changed="${this._severityChanged}"
></paper-input>
<paper-input
type="number"
label="Red"
.value="${this._severity ? this._severity.red : 0}"
.configValue=${"red"}
@value-changed="${this._severityChanged}"
></paper-input>
</div>
</div>
</div>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
.severity {
display: none;
width: 100%;
padding-left: 16px;
flex-direction: row;
flex-wrap: wrap;
}
.severity > * {
flex: 1 0 30%;
padding-right: 4px;
}
paper-toggle-button[checked] ~ .severity {
display: flex;
}
</style>
`;
}
private _toggleSeverity(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
this._config.severity = target.checked
? {
green: 0,
yellow: 0,
red: 0,
}
: undefined;
fireEvent(this, "config-changed", { config: this._config });
}
private _severityChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
const severity = {
...this._config.severity,
[target.configValue!]: Number(target.value),
};
this._config = {
...this._config,
severity,
};
fireEvent(this, "config-changed", { config: this._config });
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (target.configValue) {
if (
target.value === "" ||
(target.type === "number" && isNaN(Number(target.value)))
) {
delete this._config[target.configValue!];
} else {
let value: any = target.value;
if (target.type === "number") {
value = Number(value);
}
this._config = { ...this._config, [target.configValue!]: value };
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-gauge-card-editor": HuiGaugeCardEditor;
}
}
customElements.define("hui-gauge-card-editor", HuiGaugeCardEditor);

View File

@@ -1,11 +1,11 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { struct } from "../../common/structs/struct";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { struct } from "../../common/structs/struct";
import { processEditorEntities } from "../process-editor-entities";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
@@ -32,7 +32,6 @@ const entitiesConfigStruct = struct.union([
const cardConfigStruct = struct({
type: "string",
id: "string|number",
title: "string|number?",
theme: "string?",
columns: "number?",
@@ -49,8 +48,7 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = { type: "glance", ...config };
this._config = config;
this._configEntities = processEditorEntities(config.entities);
}
@@ -66,8 +64,8 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
return this._config!.theme || "Backend-selected";
}
get _columns(): string {
return this._config!.columns ? String(this._config!.columns) : "";
get _columns(): number {
return this._config!.columns || NaN;
}
protected render(): TemplateResult {
@@ -80,7 +78,7 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
<div class="card-config">
<paper-input
label="Title"
value="${this._title}"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
@@ -93,7 +91,8 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
></hui-theme-select-editor>
<paper-input
label="Columns"
value="${this._columns}"
type="number"
.value="${this._columns}"
.configValue="${"columns"}"
@value-changed="${this._valueChanged}"
></paper-input>
@@ -127,23 +126,29 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
}
const target = ev.target! as EditorTarget;
if (
(target.configValue! === "title" && target.value === this._title) ||
(target.configValue! === "theme" && target.value === this._theme) ||
(target.configValue! === "columns" && target.value === this._columns)
) {
if (target.configValue && this[`_${target.configValue}`] === target.value) {
return;
}
if (ev.detail && ev.detail.entities) {
this._config.entities = ev.detail.entities;
this._configEntities = processEditorEntities(this._config.entities);
} else if (target.configValue) {
this._config = {
...this._config,
[target.configValue!]:
target.checked !== undefined ? target.checked : target.value,
};
if (
target.value === "" ||
(target.type === "number" && isNaN(Number(target.value)))
) {
delete this._config[target.configValue!];
} else {
let value: any = target.value;
if (target.type === "number") {
value = Number(value);
}
this._config = {
...this._config,
[target.configValue!]:
target.checked !== undefined ? target.checked : value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}

View File

@@ -0,0 +1,111 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-iframe-card";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = struct({
type: "string",
title: "string?",
url: "string?",
aspect_ratio: "string?",
});
export class HuiIframeCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {} };
}
get _title(): string {
return this._config!.title || "";
}
get _url(): string {
return this._config!.url || "";
}
get _aspect_ratio(): string {
return this._config!.aspect_ratio || "";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<div class="side-by-side">
<paper-input
label="Title"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Aspect Ratio"
type="number"
.value="${Number(this._aspect_ratio.replace("%", ""))}"
.configValue="${"aspect_ratio"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
<paper-input
label="Url"
.value="${this._url}"
.configValue="${"url"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
let value = target.value;
if (target.configValue! === "aspect_ratio" && target.value) {
value += "%";
}
if (this[`_${target.configValue}`] === value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = { ...this._config, [target.configValue!]: value };
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-iframe-card-editor": HuiIframeCardEditor;
}
}
customElements.define("hui-iframe-card-editor", HuiIframeCardEditor);

View File

@@ -0,0 +1,113 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-light-card";
import { configElementStyle } from "./config-elements-style";
import "../../components/hui-theme-select-editor";
import "../../components/hui-entity-editor";
const cardConfigStruct = struct({
type: "string",
name: "string?",
entity: "string?",
theme: "string?",
});
export class HuiLightCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {}, _configEntities: {} };
}
get _name(): string {
return this._config!.name || "";
}
get _theme(): string {
return this._config!.theme || "default";
}
get _entity(): string {
return this._config!.entity || "";
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<paper-input
label="Name"
.value="${this._name}"
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<div class="side-by-side">
<ha-entity-picker
.hass="${this.hass}"
.value="${this._entity}"
.configValue=${"entity"}
domain-filter="light"
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-light-card-editor": HuiLightCardEditor;
}
}
customElements.define("hui-light-card-editor", HuiLightCardEditor);

View File

@@ -0,0 +1,143 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-input/paper-input";
import { struct } from "../../common/structs/struct";
import { EntitiesEditorEvent, EditorTarget } from "../types";
import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCardEditor } from "../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { Config } from "../../cards/hui-alarm-panel-card";
import { configElementStyle } from "./config-elements-style";
import { processEditorEntities } from "../process-editor-entities";
import { EntityConfig } from "../../entity-rows/types";
import "../../components/hui-entity-editor";
const entitiesConfigStruct = struct.union([
{
entity: "entity-id",
name: "string?",
icon: "icon?",
},
"entity-id",
]);
const cardConfigStruct = struct({
type: "string",
title: "string?",
aspect_ratio: "string?",
default_zoom: "number?",
entities: [entitiesConfigStruct],
});
export class HuiMapCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
private _configEntities?: EntityConfig[];
public setConfig(config: Config): void {
config = cardConfigStruct(config);
this._config = config;
this._configEntities = processEditorEntities(config.entities);
}
static get properties(): PropertyDeclarations {
return { hass: {}, _config: {}, _configEntities: {} };
}
get _title(): string {
return this._config!.title || "";
}
get _aspect_ratio(): string {
return this._config!.aspect_ratio || "";
}
get _default_zoom(): number {
return this._config!.default_zoom || NaN;
}
get _entities(): string[] {
return this._config!.entities || [];
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${configElementStyle}
<div class="card-config">
<paper-input
label="Title"
.value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<div class="side-by-side">
<paper-input
label="Aspect Ratio"
.value="${this._aspect_ratio}"
.configValue="${"aspect_ratio"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Default Zoom"
type="number"
.value="${this._default_zoom}"
.configValue="${"default_zoom"}"
@value-changed="${this._valueChanged}"
></paper-input>
</div>
<hui-entity-editor
.hass="${this.hass}"
.entities="${this._configEntities}"
@entities-changed="${this._valueChanged}"
></hui-entity-editor>
</div>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (target.configValue && this[`_${target.configValue}`] === target.value) {
return;
}
if (ev.detail && ev.detail.entities) {
this._config.entities = ev.detail.entities;
this._configEntities = processEditorEntities(this._config.entities);
} else if (target.configValue) {
if (
target.value === "" ||
(target.type === "number" && isNaN(Number(target.value)))
) {
delete this._config[target.configValue!];
} else {
let value: any = target.value;
if (target.type === "number") {
value = Number(value);
}
this._config = {
...this._config,
[target.configValue!]: value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-map-card-editor": HuiMapCardEditor;
}
}
customElements.define("hui-map-card-editor", HuiMapCardEditor);

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