Compare commits

..

206 Commits

Author SHA1 Message Date
Bram Kragten
d45f47d908 Merge pull request #9715 from home-assistant/dev
20210804.0
2021-08-04 23:53:37 +02:00
Bram Kragten
4c247ac49d Merge branch 'master' into dev 2021-08-04 20:18:00 +02:00
Bram Kragten
e8a406526b Bumped version to 20210804.0 2021-08-04 20:17:05 +02:00
Bram Kragten
7fcea16c6b Increase font size tooltips + position then static (#9713) 2021-08-04 09:19:38 -07:00
Bram Kragten
028b799d2c Default energy collection key should not be defined (#9710) 2021-08-04 08:25:42 -07:00
Bram Kragten
3485296e23 Fix typo (#9707) 2021-08-04 12:36:15 +02:00
Paulus Schoutsen
03078cdd45 TOML 2021-08-03 22:05:14 -07:00
Paulus Schoutsen
740310800d SET NODE_OPTIONS 2021-08-03 22:05:14 -07:00
Paulus Schoutsen
6d7c558482 TOML 2021-08-03 22:02:38 -07:00
Paulus Schoutsen
fdb10515c3 SET NODE_OPTIONS 2021-08-03 22:00:13 -07:00
Paulus Schoutsen
5a0f13c485 Specify YARN version on netlify (#9705) 2021-08-03 21:56:02 -07:00
Paulus Schoutsen
80b330ad7b Specify YARN version on netlify (#9705) 2021-08-03 21:55:15 -07:00
Bram Kragten
d929e1c134 Add support for statistics card to demo (#9703) 2021-08-03 15:16:21 -07:00
Bram Kragten
5e40dcdc38 Add support for statistics card to demo (#9703) 2021-08-03 15:16:08 -07:00
Paulus Schoutsen
1dd3e2a83b 20210803.2 (#9704)
* Add energy distribution card to arsaboo demo (#9702)

* Add energy distribution card to arsaboo demo

* Make link dashboard conform

* Bumped version to 20210803.2
2021-08-03 15:15:21 -07:00
Paulus Schoutsen
a62742fad9 Bumped version to 20210803.2 2021-08-03 15:14:46 -07:00
Paulus Schoutsen
1f9c45b11c Add energy distribution card to arsaboo demo (#9702)
* Add energy distribution card to arsaboo demo

* Make link dashboard conform
2021-08-03 22:09:08 +00:00
Bram Kragten
68bec5e158 Merge pull request #9701 from home-assistant/dev
20210803.1
2021-08-03 23:36:27 +02:00
Bram Kragten
37f1bd7d63 Bumped version to 20210803.1 2021-08-03 23:22:46 +02:00
Bram Kragten
5ba24211e2 Add messages when no data (#9700)
* Add messages when no data

* 2 hours

* comment
2021-08-03 23:22:32 +02:00
Bram Kragten
d699647418 Fix calculate sum growth + refreshing too often (#9699) 2021-08-03 23:19:26 +02:00
Bram Kragten
b246502cb6 Energy demo (#9698)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-08-03 09:50:31 -07:00
Bram Kragten
9b33ead8aa Final energy tweaks (#9694) 2021-08-03 08:54:10 -07:00
Milan Meulemans
32e8c1dc6d Remove energy currency translation (#9695) 2021-08-03 11:00:21 +00:00
Paulus Schoutsen
e09ef7862e Merge pull request #9692 from home-assistant/dev 2021-08-02 21:10:24 -07:00
Paulus Schoutsen
85420304d0 Bumped version to 20210803.0 2021-08-02 20:51:41 -07:00
Bram Kragten
1c097a669d Add currency to onboarding (#9691) 2021-08-02 21:02:57 +00:00
Bram Kragten
4e1497c5da Change layout of period selector (#9688) 2021-08-02 13:57:59 -07:00
Bram Kragten
49d426675f Masonry (#9689) 2021-08-02 13:57:39 -07:00
Paulus Schoutsen
dc6ac668b4 Merge pull request #9687 from home-assistant/add-description-devices-dialog
Add description to statistic picker in add device dialog
2021-08-02 10:30:24 -07:00
Paulus Schoutsen
4ee24b0845 Add description to statistic picker in add device dialog 2021-08-02 10:18:59 -07:00
Paulus Schoutsen
ba20aef206 Merge pull request #9685 from home-assistant/dev 2021-08-02 10:07:14 -07:00
Paulus Schoutsen
41ef6133c1 Bumped version to 20210802.0 2021-08-02 09:51:10 -07:00
Joakim Sørensen
50bd5ee8f7 Add my support for energy (#9681)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-08-02 09:50:47 -07:00
Joakim Sørensen
285f3fe330 Fix text color for hass-tabs-subpage with narrow (#9680) 2021-08-02 09:50:24 -07:00
Bram Kragten
4d01199986 Clear all energy collection prefs (#9684) 2021-08-02 09:39:01 -07:00
Paulus Schoutsen
bcc0052fe0 Update text 2021-08-02 08:45:05 -07:00
Paulus Schoutsen
4b592d81bd Simplify date picker and put in sidebar (#9677)
* Simplify date picker and put in sidebar

* Convert picker to reusable component

* Put date picker in top bar on desktop

* Chefs
2021-08-02 08:18:49 -07:00
Bram Kragten
884e323288 Make clear it is showing today (#9679) 2021-08-02 07:22:36 -07:00
Bram Kragten
78b799dd05 Add cost stat ids (#9678) 2021-08-02 07:22:17 -07:00
Paulus Schoutsen
847fa2e700 Add energy distribution card to auto lovelace if energy configured (#9675)
* Add energy distribution card to auto lovelace if energy configured

* Add import

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-08-02 12:36:13 +00:00
Paulus Schoutsen
481a79e311 Allow specifying collection keys so energy dashboard has own (#9674) 2021-08-02 14:23:15 +02:00
Paulus Schoutsen
f19f2ff321 Add tooltips to the gauges (#9676) 2021-08-02 14:21:41 +02:00
Bram Kragten
6dc4d7bb70 Merge pull request #9673 from home-assistant/dev
20210801.0
2021-08-01 23:40:48 +02:00
Bram Kragten
83460a34f4 Bumped version to 20210801.0 2021-08-01 23:25:24 +02:00
Bram Kragten
2adbb47373 Refresh stats at 20 minutes past the hour every hour (#9667) 2021-08-01 21:24:41 +00:00
Bram Kragten
2159a5419a Fix device energy graph height (#9666)
* Fix device energy graph height

* Update state-history-chart-timeline.ts

* Update hui-energy-devices-graph-card.ts
2021-08-01 13:59:34 -07:00
Bram Kragten
044d6a15d9 Add view type selector to view editor (#9671) 2021-08-01 22:55:51 +02:00
Bram Kragten
b6055062c6 Don's start at zero so growth is beter visible + calc sum state correctly (#9672) 2021-08-01 22:55:41 +02:00
Bram Kragten
6fd85e043b Sidebar view: Allow to move card position from UI (#9669) 2021-08-01 18:17:34 +02:00
Bram Kragten
e07ac52356 Add UOM to stats chart, fix coloring of bands (#9665) 2021-07-31 20:36:27 +02:00
Paulus Schoutsen
0f16ba9325 Speed up data loading and allow embedding individual energy cards (#9660)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-07-31 18:33:41 +02:00
Bram Kragten
378e6d28bc Merge pull request #9659 from home-assistant/dev 2021-07-30 22:29:51 +02:00
Bram Kragten
539d2b880c Bumped version to 20210730.0 2021-07-30 22:14:21 +02:00
Bram Kragten
2982adbfa7 Fix return compensation not being negative (#9654) 2021-07-30 12:57:27 -07:00
Bram Kragten
5147dff670 Sidebar view: Move all cards to 1 column on mobile (#9656) 2021-07-30 12:57:01 -07:00
Bram Kragten
2cdf78c504 Add grid neutrality gauge (#9655) 2021-07-30 12:55:58 -07:00
Bram Kragten
cfad45b7c2 fix and finish statistics card (#9658) 2021-07-30 12:52:38 -07:00
Bram Kragten
5234e9bce5 still round numbers when not formatting them (#9653) 2021-07-30 17:36:31 +02:00
Bram Kragten
0ed5454d02 fix self consumed solar (#9651) 2021-07-30 12:28:47 +02:00
Thomas Lovén
03080973be Add needle option to ha-gauge and gauge card (#9637) 2021-07-30 10:51:21 +02:00
Paulus Schoutsen
a4f51b0cb3 Fix label for device consumption (#9648) 2021-07-30 09:42:54 +02:00
Paulus Schoutsen
749079c1c3 Bumped version to 20210729.0 2021-07-29 12:02:34 -07:00
Bram Kragten
ae10ff42e1 Update state-history-chart-line.ts 2021-07-29 20:45:25 +02:00
Bram Kragten
d4cbdab4a3 Fix energy calculations (#9647)
* Fix calculations

* max.. not min...
2021-07-29 18:29:49 +00:00
Bram Kragten
1bd6392a4c Add text when no statistics found (#9642)
* Add text when no statistics found

* Update src/components/entity/ha-statistic-picker.ts

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

* fix typos

* Update src/components/entity/ha-statistic-picker.ts

* Prettier

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-07-29 20:26:22 +02:00
Bram Kragten
1531e99528 Energy dashboard tweaks and fixes (#9643)
* Energy dashboard tweaks and fixes

* Make headers smaller

* Change button styling in onboarding

* Disable add when no stat choosen

* Oops

* Update hui-energy-carbon-consumed-gauge-card.ts

* Update hui-energy-distribution-card.ts
2021-07-29 10:44:33 -07:00
Julien Ehrhart
6e7af18494 Add support for DEVICE_CLASS_MONETARY (#9640) 2021-07-29 15:20:12 +00:00
Paulus Schoutsen
e1bae65aeb Remove currency from initial config (#9638) 2021-07-29 17:12:49 +02:00
Paulus Schoutsen
65bfdf94c9 Bumped version to 20210728.0 2021-07-28 11:05:28 -07:00
Paulus Schoutsen
9a92825954 Move energy panel up in sidebar (#9636)
* Move energy panel up in sidebar

* Remove headers from wizard

* Update text

* Always show all configured devices

* Make leaf clickable

* Bump HAWS
2021-07-28 11:05:00 -07:00
Paulus Schoutsen
469faf509b Remove the energy summary card since it's already in other cards (#9635) 2021-07-28 10:46:59 -07:00
Paulus Schoutsen
f87d4a5ab6 If we have solar defined, always make sure sum is number so it shows placeholders (#9634) 2021-07-28 10:46:51 -07:00
Bram Kragten
9c31b749d7 Format monetary sensor as currency (#9633) 2021-07-28 09:34:03 -07:00
Paulus Schoutsen
521d5df064 Show solar forecast even if no prod stats available (#9632) 2021-07-28 09:20:31 -07:00
Paulus Schoutsen
4d330fba8a Use historic CO2 data into account (#9626)
* Use historic CO2 data into account

* Also add to gauge

* Apply suggestions from code review

* Format

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-07-28 09:20:06 -07:00
Bram Kragten
2e04a55d5c Add statistics card to card picker (#9631)
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2021-07-28 08:41:18 -07:00
Bram Kragten
0d9d0aa18b Add legend to electricity graph, fix text color in table (#9629) 2021-07-28 08:40:43 -07:00
Bram Kragten
f8dd1795bc Add animation to distribution (#9625) 2021-07-28 08:39:12 -07:00
Bram Kragten
1d7007584c Use currency from core config (#9628) 2021-07-28 08:38:04 -07:00
Charles Garwood
8cd9f891fb Add remove failed node support to Z-Wave JS devices (#9560)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-07-28 13:38:50 +00:00
Franck Nijhof
6ab0f1db57 Add currency core configuration (#9620)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-07-28 12:48:39 +02:00
Paulus Schoutsen
37d754d069 Bumped version to 20210727.0 2021-07-27 15:25:12 -07:00
Charles Garwood
e12b194d41 Add Z-Wave JS Heal Node wizard (#9562) 2021-07-28 00:16:19 +02:00
Michelle Fuchs
07d7fa26fe Show value of 'Before'-time in automation editor #9142 (#9598) 2021-07-28 00:13:38 +02:00
Bram Kragten
73b9b87ef3 Update energy dashboard (#9624) 2021-07-27 13:57:09 -07:00
Bram Kragten
0c0091375c Fix grid onboarding (#9621) 2021-07-27 21:34:43 +02:00
Bram Kragten
21a29ed3a5 Statistic sums requires at least 2 values (#9616) 2021-07-26 22:47:50 +00:00
Bram Kragten
a6312f4279 Fix sidebar view edit mode (#9615) 2021-07-26 22:11:34 +00:00
Bram Kragten
f459abdf85 Fix statistics graph (#9607) 2021-07-26 15:03:31 -07:00
Bram Kragten
586fa1d0f0 Fix zoom setting in maps card (#9613)
Fixes #9611
2021-07-26 15:02:50 -07:00
Bram Kragten
bf4192a1f0 Bump Vaadin elements (#9609) 2021-07-26 15:02:23 -07:00
Bram Kragten
ac31eedf65 Bump material elements (#9610) 2021-07-26 15:02:01 -07:00
Bram Kragten
b05dc5141c Fix chart tooltip footer always rendering (#9614) 2021-07-26 15:01:32 -07:00
Raman Gupta
32c6fb14dd Re-add success/failure indicator on call service button in dev tools (#9600)
* Re-add success/failure indicator on call service button in dev tools

* move success outside of try block

* Export HaProgressButton
2021-07-26 23:59:49 +02:00
Raman Gupta
982c940381 Try to fix download of zwave_js logs (#9612) 2021-07-26 23:26:22 +02:00
Bram Kragten
a7a8aaa887 Merge pull request #9608 from home-assistant/dev
20210726.0
2021-07-26 23:04:05 +02:00
Philip Allgaier
bf83a9980e Add bottom margin to <ha-map> in more-info-person (#9604) 2021-07-26 22:45:23 +02:00
Bram Kragten
11be603ed3 Bumped version to 20210726.0 2021-07-26 22:18:41 +02:00
Bram Kragten
a432cf8405 Fix selected icon of picker elements (#9606) 2021-07-26 22:17:29 +02:00
Bram Kragten
9dd6b3b72d Add Energy panel (#9445)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-07-26 09:57:59 -07:00
Thomas Lovén
faca62b55f Avoid gaps in grid card with conditional (#9507)
* Avoid gaps in grid card with conditional. Fix #9433

* Rename hidden parameter

* Restore devcontainer.json

* Fix accidental change

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

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

* Prettier

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-07-26 14:43:14 +02:00
J. Nick Koston
a339de89f5 Add support for lock unlocking,locking and jammed states (#9537) 2021-07-20 10:52:24 -10:00
Charles Garwood
e40c90e9c0 Translate Z-Wave JS Re-Interview Device button (#9566) 2021-07-19 11:23:10 +02:00
Bram Kragten
3f447bb8a7 Bump Polymer (#9525)
* Bump Polymer

* Remove formatting from patch

* Cleanup

* Fix Vaadin

* Fix typing
2021-07-16 01:23:09 +02:00
Bram Kragten
21dca3fbf8 Use patch for @lit-labs/virtualizer (#9524)
* Use patch for `@lit-labs/virtualizer`

* Re-organize

* Use original for patch

* Also patch import of EventTarget polyfill

* Delete EventTarget-ponyfill.js

* Prettier
2021-07-16 01:16:24 +02:00
Bram Kragten
1078bb4287 Add statistics-graph-card (#9479)
* Add `statistics-graph-card`

* Make variable names clearer
2021-07-16 01:16:02 +02:00
Charles Garwood
daeed06e70 Clean up Z-Wave JS dialog CSS (#9561) 2021-07-16 01:13:00 +02:00
Bram Kragten
1206e2d75f Change Data Entry Flow loading step description logic + cleanups (#9558)
* Change Data Entry Flow loading step description logic + cleanups

Fixes #6251

* Lint

* Address comment
2021-07-15 21:07:25 +02:00
Bram Kragten
cc81239b9d Add struct for fire-dom-event action (#9556) 2021-07-15 12:08:33 +02:00
Bram Kragten
e797c01761 Update lint rules (#9563)
Updated deps

Added `unused-imports`, prefer arrow, and import order
2021-07-15 12:08:04 +02:00
Charles Garwood
12f7366968 Replace home ID with config entry title in Z-Wave device Info (#9488) 2021-07-13 18:05:22 +02:00
Bram Kragten
b7fd7abe85 increase memory demo action (#9553) 2021-07-13 10:15:59 +02:00
Jason Hunter
a5e1f3d165 bump hls.js (#9546)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-07-13 09:00:51 +02:00
Joakim Sørensen
212d047ada Place Z-Wave JS stages in grid (#9548) 2021-07-12 12:44:07 -04:00
Bram Kragten
2e16127fde Inline mdi icons with babel plugin + bump build deps (#9498) 2021-07-12 09:26:07 -07:00
Charles Garwood
e8b53a619d Show parameter number on ZWaveJS device config panel (#9494) 2021-07-12 09:41:54 +02:00
posixx
df1ca1fd96 HA frontend change for alarm panel vacation mode (#8326)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-07-08 16:21:09 +02:00
Bram Kragten
8f85132d48 Download translations on release (#9530)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-07-08 13:04:38 +00:00
Marius
349144599c Change text on Yaml configuration reloading (#9529)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-07-08 13:04:17 +00:00
Thomas Lovén
979093923b Colorize trace paths for choose without explicit default case (#9527)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-07-08 00:15:20 +02:00
Thomas Lovén
137f8ad4cb Revert "Colorize trace paths for choose without explicit default case"
This reverts commit c1f462b8f8.
2021-07-07 21:39:33 +00:00
Thomas Lovén
c1f462b8f8 Colorize trace paths for choose without explicit default case 2021-07-07 21:28:13 +00:00
Bram Kragten
6701c4c371 Dedupe dependencies (#9523)
+ yarn bump + CI check for duplicate dependencies
2021-07-07 17:31:37 +02:00
Bram Kragten
fc6e459c09 Upgrade to yarn 2 (#9500) 2021-07-07 16:33:24 +02:00
Philip Allgaier
9e28b3447e Fixed "ID" spelling in trigger field label (#9518) 2021-07-07 11:07:57 +02:00
Bram Kragten
5c737e1969 Merge pull request #9517 from home-assistant/dev 2021-07-07 09:58:19 +02:00
Bram Kragten
569765e77e Bumped version to 20210707.0 2021-07-07 09:43:20 +02:00
Bram Kragten
bc0d63ed12 Better fix for Safari IBD bug (#9514)
* Better fix for Safari IBD bug

* comment
2021-07-07 09:42:41 +02:00
GitHub Action
02f9893522 Translation update 2021-07-07 00:47:35 +00:00
Bram Kragten
b4bbe63f0f Fix device trigger clearing trigger id (#9511) 2021-07-06 11:53:21 +02:00
Bram Kragten
fabbcac99f Merge pull request #9510 from home-assistant/dev 2021-07-06 11:09:42 +02:00
Bram Kragten
b1b5ab6949 Bumped version to 20210706.0 2021-07-06 10:47:41 +02:00
Bram Kragten
4b9487183b Add tracing to scripts (#9486) 2021-07-06 10:46:51 +02:00
Bram Kragten
de5a817953 Add UI for trigger condition (#9505) 2021-07-06 10:43:07 +02:00
GitHub Action
4970f640fa Translation update 2021-07-06 00:47:02 +00:00
Bram Kragten
18996535b7 Fix race in translations loading (#9499) 2021-07-05 11:05:49 +02:00
GitHub Action
2a1e31b5e9 Translation update 2021-07-05 00:47:09 +00:00
GitHub Action
8ca9a0f409 Translation update 2021-07-04 00:47:10 +00:00
GitHub Action
fcc89a67ba Translation update 2021-07-03 00:46:50 +00:00
GitHub Action
1f377d43c5 Translation update 2021-07-02 00:47:14 +00:00
Joakim Sørensen
30d6c68908 Fix writing supervisor entrypoint (#9489) 2021-07-01 11:34:22 +02:00
Bram Kragten
dc781da93a Use ES5 build for Supervisor on Safari 12 and below (#9485) 2021-07-01 09:27:02 +02:00
Bram Kragten
36c20e4348 Limit height of charts to 400px (#9487) 2021-07-01 07:54:17 +02:00
GitHub Action
4466950bb8 Translation update 2021-07-01 00:47:12 +00:00
Joakim Sørensen
be29828454 Change path for codespaces (#9484) 2021-06-30 15:58:01 +02:00
Bram Kragten
7bab245073 Merge pull request #9483 from home-assistant/dev 2021-06-30 12:17:54 +02:00
Bram Kragten
f5dcf0b760 Bumped version to 20210630.0 2021-06-30 12:03:07 +02:00
Bram Kragten
8141f78a92 Use ES5 build for Safari 12 and below (#9482) 2021-06-30 12:02:01 +02:00
Joakim Sørensen
be244b3d00 Rename hassos -> haos (#9477) 2021-06-30 12:00:33 +02:00
Bram Kragten
805f5ff9b6 Recreate columns if cards change (#9480) 2021-06-30 11:52:36 +02:00
Bram Kragten
76daeb7e55 Fix wait for not loaded panel (#9478) 2021-06-30 11:50:49 +02:00
GitHub Action
9594c8106e Translation update 2021-06-30 00:47:15 +00:00
Joakim Sørensen
ed4052365c Allow placeholders in config and option flows (#9314) 2021-06-30 01:09:18 +02:00
Joakim Sørensen
377ebadc10 Show note about integrations not in UI even for non-advanced (#9457)
* Show note about integrations not in UI even for non-advanced

* Address comments
2021-06-30 01:08:58 +02:00
Charles Garwood
ed4809b71e Handle config entry not loaded on Z-Wave JS config panel (#9451)
* Handle config entry not loaded on Z-Wave JS config panel

* Move ERROR_STATES to data/config_entries and import
2021-06-29 19:04:42 -04:00
Shane Qi
db37dffdbb Fixed Logbook Card/Panel/Dialog Incorrect Entires for input_datetime Entities (#9399)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-30 00:32:19 +02:00
Bram Kragten
13cab7e301 Fix logbook card (#9476) 2021-06-29 18:32:51 +02:00
Charles Garwood
0a50fc66e5 Add ZWave JS heal network UI (#9449)
* Add Z-Wave JS heal network dialog

* progress bar tweak

* tweak package.json

* typing tweak

* Update yarn.lock

* Align versions

* address review comments

* Use indeterminate linear-progress instead of circular-progress

* cleanup circular-progress

* prettier

* additional review cleanup

* subscribe to status update if heal already running

* more cleanup

* more cleanup

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-29 11:14:50 -04:00
uvjustin
a3d1a3566d Fix ha-hls-player cleanup for lit 2 (#9388)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-29 15:44:18 +02:00
Philip Allgaier
ba0be927ed Set min value for history graph card rendered hours (#9464) 2021-06-29 15:01:42 +02:00
Philip Allgaier
4260606267 Set minimum = 1 hours to show (#9466) 2021-06-29 15:01:18 +02:00
Philip Allgaier
4665db4f27 Fix integration card rename dialog logic (#9467) 2021-06-29 15:00:27 +02:00
Franck Nijhof
43503ba085 Fix number entity row availability when state unknown (#9475) 2021-06-29 14:57:39 +02:00
Joakim Sørensen
0a83a704f1 Ignore previous versions in add-on changelog (#9474) 2021-06-29 14:57:25 +02:00
GitHub Action
08de941c90 Translation update 2021-06-29 00:47:16 +00:00
GitHub Action
62228ef144 Translation update 2021-06-28 00:47:17 +00:00
GitHub Action
9731257782 Translation update 2021-06-27 00:47:51 +00:00
GitHub Action
4ec9c9c16e Translation update 2021-06-26 00:46:57 +00:00
Franck Nijhof
45436731e2 Fix select entity disabled when no item selected (#9465) 2021-06-24 21:36:23 -07:00
GitHub Action
27730e65e7 Translation update 2021-06-25 00:47:31 +00:00
rianadon
a4aba93d57 Add input elements to login page for password managers (#9369) 2021-06-24 23:14:36 +02:00
Martin Hjelmare
d93db16963 Add button for zwave_js options flow (#9001)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-06-24 13:21:30 +02:00
GitHub Action
c327fe11b8 Translation update 2021-06-23 00:48:18 +00:00
GitHub Action
4fbc31d0b0 Translation update 2021-06-22 00:48:29 +00:00
Bram Kragten
9a4a1cb4ec Fix charts tooltips and legends (#9448) 2021-06-21 10:38:50 +02:00
Joakim Sørensen
202d6957bc Allow clearing values in optional selects (#9442)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-21 09:41:38 +02:00
Joakim Sørensen
14fcff7774 Fix secrets schema (#9446) 2021-06-21 09:41:06 +02:00
GitHub Action
2c9aa1cab4 Translation update 2021-06-21 00:48:39 +00:00
GitHub Action
7745c10d07 Translation update 2021-06-20 00:48:37 +00:00
Franck Nijhof
c1d571de42 Add Select entity (#9422)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-06-19 13:26:19 +02:00
GitHub Action
cecb66451c Translation update 2021-06-19 00:48:32 +00:00
Bram Kragten
0ef3421fa2 Bump chartjs to version 3 (#9401) 2021-06-18 11:15:07 +02:00
GitHub Action
f88e238d41 Translation update 2021-06-18 00:48:41 +00:00
Raman Gupta
ce3c8f264d Update zwave_js log subscription to handle core changes (#9262) 2021-06-17 17:19:18 +02:00
Bram Kragten
9fbd594f37 Check if /proc/version exists (#9438) 2021-06-17 15:23:20 +02:00
Bram Kragten
5ad95cad90 Support white color mode (#9386) 2021-06-17 15:23:08 +02:00
GitHub Action
7e507b40c4 Translation update 2021-06-17 00:48:10 +00:00
Joakim Sørensen
446a9b5c02 Don't clear action on expected error (#9428) 2021-06-16 11:13:30 +02:00
GitHub Action
e02e61384e Translation update 2021-06-16 00:48:25 +00:00
Raman Gupta
5deb570fdf Add button to download logs from zwave_js logs page (#9395) 2021-06-16 00:02:40 +02:00
Joakim Sørensen
915c46f144 Fix add-on configuration validation (#9424) 2021-06-15 21:00:28 +02:00
Philip Allgaier
30d6c5eaf3 Gracefully handle logbook retrieval errors (#9377) 2021-06-15 19:54:18 +02:00
Philip Allgaier
6e50d1166a Only attempt to get "trace/contexts" if admin (#9378)
* Only attempt to get "trace/contexts" if admin

* Changes from review
2021-06-15 16:38:37 +02:00
Bram Kragten
8666e6baae Merge pull request #9358 from home-assistant/dev 2021-06-03 23:12:30 +02:00
Bram Kragten
2db9f33c41 Merge pull request #9334 from home-assistant/dev 2021-06-01 21:53:08 +02:00
Bram Kragten
3d788d6056 Merge pull request #9324 from home-assistant/dev 2021-06-01 11:56:02 +02:00
Paulus Schoutsen
7560f98d70 Merge pull request #9320 from home-assistant/dev 2021-05-31 15:55:09 -07:00
Paulus Schoutsen
1533c22d5c Merge pull request #9310 from home-assistant/dev 2021-05-30 20:30:44 -07:00
Bram Kragten
cf03f103ab Merge pull request #9285 from home-assistant/dev 2021-05-28 12:45:21 +02:00
Bram Kragten
4a8a7c997e Merge pull request #9267 from home-assistant/dev 2021-05-26 17:33:32 +02:00
Bram Kragten
9612bc78fe Merge pull request #9097 from home-assistant/dev 2021-05-04 23:21:05 +02:00
Bram Kragten
2b86137388 Merge pull request #9079 from home-assistant/dev 2021-05-03 16:16:58 +02:00
Paulus Schoutsen
8fdbe447c1 Merge pull request #9060 from home-assistant/dev
20210430.0
2021-04-30 12:43:33 -07:00
Bram Kragten
764ae7e0b6 Merge pull request #9045 from home-assistant/dev 2021-04-29 22:21:03 +02:00
Paulus Schoutsen
6b7e78320d Merge pull request #9024 from home-assistant/dev 2021-04-28 10:47:16 -07:00
480 changed files with 38121 additions and 22025 deletions

View File

@@ -35,55 +35,51 @@
"es6": true "es6": true
}, },
"rules": { "rules": {
"class-methods-use-this": 0, "class-methods-use-this": "off",
"new-cap": 0, "new-cap": "off",
"prefer-template": 0, "prefer-template": "off",
"object-shorthand": 0, "object-shorthand": "off",
"func-names": 0, "func-names": "off",
"prefer-arrow-callback": 0, "no-underscore-dangle": "off",
"no-underscore-dangle": 0, "strict": "off",
"strict": 0, "no-plusplus": "off",
"prefer-spread": 0, "no-bitwise": "error",
"no-plusplus": 0, "comma-dangle": "off",
"no-bitwise": 2, "vars-on-top": "off",
"comma-dangle": 0, "no-continue": "off",
"vars-on-top": 0, "no-param-reassign": "off",
"no-continue": 0, "no-multi-assign": "off",
"no-param-reassign": 0, "no-console": "error",
"no-multi-assign": 0, "radix": "off",
"no-console": 2, "no-alert": "off",
"radix": 0, "no-nested-ternary": "off",
"no-alert": 0, "prefer-destructuring": "off",
"no-return-await": 0,
"no-nested-ternary": 0,
"prefer-destructuring": 0,
"no-restricted-globals": [2, "event"], "no-restricted-globals": [2, "event"],
"prefer-promise-reject-errors": 0, "prefer-promise-reject-errors": "off",
"import/order": 0, "import/prefer-default-export": "off",
"import/prefer-default-export": 0, "import/no-default-export": "off",
"import/no-unresolved": 0, "import/no-unresolved": "off",
"import/no-cycle": 0, "import/no-cycle": "off",
"import/extensions": [ "import/extensions": [
2, "error",
"ignorePackages", "ignorePackages",
{ "ts": "never", "js": "never" } { "ts": "never", "js": "never" }
], ],
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"], "no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"object-curly-newline": 0, "object-curly-newline": "off",
"default-case": 0, "default-case": "off",
"wc/no-self-class": 0, "wc/no-self-class": "off",
"no-shadow": 0, "no-shadow": "off",
"@typescript-eslint/camelcase": 0, "@typescript-eslint/camelcase": "off",
"@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-use-before-define": 0, "@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-non-null-assertion": 0, "@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": 0, "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-shadow": ["error"], "@typescript-eslint/no-shadow": ["error"],
"@typescript-eslint/naming-convention": [ "@typescript-eslint/naming-convention": [
0, "off",
{ {
"selector": "default", "selector": "default",
"format": ["camelCase", "snake_case"], "format": ["camelCase", "snake_case"],
@@ -101,9 +97,20 @@
"format": ["PascalCase"] "format": ["PascalCase"]
} }
], ],
"lit/attribute-value-entities": 0 "@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-vars": [
"error",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"unused-imports/no-unused-imports": "error",
"lit/attribute-value-entities": "off"
}, },
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"], "plugins": ["disable", "unused-imports"],
"processor": "disable/disable", "processor": "disable/disable"
"ignorePatterns": ["src/resources/lit-virtualizer/*"]
} }

View File

@@ -10,26 +10,21 @@ on:
- dev - dev
- master - master
env:
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=4096
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setting up Node.js - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: 12.x node-version: ${{ env.NODE_VERSION }}
- name: Get yarn cache path cache: yarn
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
env: env:
@@ -42,51 +37,35 @@ jobs:
run: yarn run lint:types run: yarn run lint:types
- name: Run prettier - name: Run prettier
run: yarn run lint:prettier run: yarn run lint:prettier
- name: Check for duplicate dependencies
run: yarn dedupe --check
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setting up Node.js - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: 12.x node-version: ${{ env.NODE_VERSION }}
- name: Get yarn cache path cache: yarn
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
env: env:
CI: true CI: true
- name: Run Mocha - name: Run Mocha
run: npm run mocha run: yarn run mocha
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [lint, test] needs: [lint, test]
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setting up Node.js - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: 12.x node-version: ${{ env.NODE_VERSION }}
- name: Get yarn cache path cache: yarn
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
env: env:
@@ -101,20 +80,11 @@ jobs:
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setting up Node.js - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: 12.x node-version: ${{ env.NODE_VERSION }}
- name: Get yarn cache path cache: yarn
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
env: env:

View File

@@ -4,26 +4,22 @@ on:
push: push:
branches: branches:
- dev - dev
env:
NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=4096
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setting up Node.js - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: 12.x node-version: ${{ env.NODE_VERSION }}
- name: Get yarn cache path cache: yarn
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Fetching Yarn cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install
env: env:

View File

@@ -7,7 +7,8 @@ on:
env: env:
PYTHON_VERSION: 3.8 PYTHON_VERSION: 3.8
NODE_VERSION: 12.1 NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=4096
jobs: jobs:
release: release:
@@ -29,7 +30,15 @@ jobs:
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package - name: Build and release package
run: | run: |
python3 -m pip install twine python3 -m pip install twine

View File

@@ -1,8 +1,6 @@
name: Translations name: Translations
on: on:
schedule:
- cron: "30 0 * * *"
push: push:
branches: branches:
- dev - dev
@@ -10,7 +8,7 @@ on:
- src/translations/en.json - src/translations/en.json
env: env:
NODE_VERSION: 12 NODE_VERSION: 14
jobs: jobs:
upload: upload:
@@ -20,46 +18,8 @@ jobs:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
- name: Upload Translations - name: Upload Translations
run: | run: |
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}" export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
./script/translations_upload_base ./script/translations_upload_base
download:
name: Download
needs: upload
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v2
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
- name: Download Translations
run: |
export LOKALISE_TOKEN="${{ secrets.LOKALISE_TOKEN }}"
npm install
./script/translations_download
- name: Initialize git
uses: home-assistant/actions/helpers/git-init@master
with:
name: GitHub Action
email: github-action@users.noreply.github.com
- name: Update translation
run: |
git add translations
git commit -am "Translation update"
git push

10
.gitignore vendored
View File

@@ -8,9 +8,15 @@ hass_frontend/*
dist dist
# yarn # yarn
.yarn .yarn/*
yarn-error.log !.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
node_modules/* node_modules/*
yarn-error.log
npm-debug.log npm-debug.log
# Python stuff # Python stuff

2
.nvmrc
View File

@@ -1 +1 @@
12.1 14

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
diff --git a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
index d92179f7fd5315203f870a6963e871dc8ddf6c0c..362e284121b97e0fba0925225777aebc32e26b8d 100644
--- a/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
+++ b/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js
@@ -1,14 +1,15 @@
-let _ET, ET;
+let _ET;
+let ET;
export default async function EventTarget() {
- return ET || init();
+ return ET || init();
}
async function init() {
- _ET = window.EventTarget;
- try {
- new _ET();
- }
- catch (_a) {
- _ET = (await import('event-target-shim')).EventTarget;
- }
- return (ET = _ET);
+ _ET = window.EventTarget;
+ try {
+ new _ET();
+ } catch (_a) {
+ _ET = (await import("event-target-shim")).default.EventTarget;
+ }
+ return (ET = _ET);
}

View File

@@ -0,0 +1,34 @@
diff --git a/lib/legacy/class.js b/lib/legacy/class.js
index aee2511be1cd9bf900ee552bc98190c1631c57c0..f2f499d68bf52034cac9c28307c99e8ce6b8417d 100644
--- a/lib/legacy/class.js
+++ b/lib/legacy/class.js
@@ -304,17 +304,23 @@ function GenerateClassFromInfo(info, Base, behaviors) {
// only proceed if the generated class' prototype has not been registered.
const generatedProto = PolymerGenerated.prototype;
if (!generatedProto.hasOwnProperty(JSCompiler_renameProperty('__hasRegisterFinished', generatedProto))) {
- generatedProto.__hasRegisterFinished = true;
+ // make sure legacy lifecycle is called on the *element*'s prototype
+ // and not the generated class prototype; if the element has been
+ // extended, these are *not* the same.
+ const proto = Object.getPrototypeOf(this);
+ // Only set flag when generated prototype itself is registered,
+ // as this element may be extended from, and needs to run `registered`
+ // on all behaviors on the subclass as well.
+ if (proto === generatedProto) {
+ generatedProto.__hasRegisterFinished = true;
+ }
// ensure superclass is registered first.
super._registered();
// copy properties onto the generated class lazily if we're optimizing,
- if (legacyOptimizations) {
+ if (legacyOptimizations && !Object.hasOwnProperty(generatedProto, '__hasCopiedProperties')) {
+ generatedProto.__hasCopiedProperties = true;
copyPropertiesToProto(generatedProto);
}
- // make sure legacy lifecycle is called on the *element*'s prototype
- // and not the generated class prototype; if the element has been
- // extended, these are *not* the same.
- const proto = Object.getPrototypeOf(this);
let list = lifecycle.beforeRegister;
if (list) {
for (let i=0; i < list.length; i++) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

55
.yarn/releases/yarn-2.4.2.cjs vendored Executable file

File diff suppressed because one or more lines are too long

9
.yarnrc.yml Normal file
View File

@@ -0,0 +1,9 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-2.4.2.cjs

View File

@@ -0,0 +1,170 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path");
// Currently only supports CommonJS modules, as require is synchronous. `import` would need babel running asynchronous.
module.exports = function inlineConstants(babel, options, cwd) {
const t = babel.types;
if (!Array.isArray(options.modules)) {
throw new TypeError(
"babel-plugin-inline-constants: expected a `modules` array to be passed"
);
}
if (options.resolveExtensions && !Array.isArray(options.resolveExtensions)) {
throw new TypeError(
"babel-plugin-inline-constants: expected `resolveExtensions` to be an array"
);
}
const ignoreModuleNotFound = options.ignoreModuleNotFound;
const resolveExtensions = options.resolveExtensions;
const hasRelativeModules = options.modules.some(
(module) => module.startsWith(".") || module.startsWith("/")
);
const modules = Object.fromEntries(
options.modules.map((module) => {
const absolute = module.startsWith(".")
? require.resolve(module, { paths: [cwd] })
: module;
// eslint-disable-next-line import/no-dynamic-require
return [absolute, require(absolute)];
})
);
const toLiteral = (value) => {
if (typeof value === "string") {
return t.stringLiteral(value);
}
if (typeof value === "number") {
return t.numericLiteral(value);
}
if (typeof value === "boolean") {
return t.booleanLiteral(value);
}
if (value === null) {
return t.nullLiteral();
}
throw new Error(
"babel-plugin-inline-constants: cannot handle non-literal `" + value + "`"
);
};
const resolveAbsolute = (value, state, resolveExtensionIndex) => {
if (!state.filename) {
throw new TypeError(
"babel-plugin-inline-constants: expected a `filename` to be set for files"
);
}
if (resolveExtensions && resolveExtensionIndex !== undefined) {
value += resolveExtensions[resolveExtensionIndex];
}
try {
return require.resolve(value, { paths: [path.dirname(state.filename)] });
} catch (error) {
if (
error.code === "MODULE_NOT_FOUND" &&
resolveExtensions &&
(resolveExtensionIndex === undefined ||
resolveExtensionIndex < resolveExtensions.length - 1)
) {
const resolveExtensionIdx = (resolveExtensionIndex || -1) + 1;
return resolveAbsolute(value, state, resolveExtensionIdx);
}
if (error.code === "MODULE_NOT_FOUND" && ignoreModuleNotFound) {
return undefined;
}
throw error;
}
};
const importDeclaration = (p, state) => {
if (p.node.type !== "ImportDeclaration") {
return;
}
const absolute =
hasRelativeModules && p.node.source.value.startsWith(".")
? resolveAbsolute(p.node.source.value, state)
: p.node.source.value;
if (!absolute || !(absolute in modules)) {
return;
}
const module = modules[absolute];
for (const specifier of p.node.specifiers) {
if (
specifier.type === "ImportDefaultSpecifier" &&
specifier.local &&
specifier.local.type === "Identifier"
) {
if (!("default" in module)) {
throw new Error(
"babel-plugin-inline-constants: cannot access default export from `" +
p.node.source.value +
"`"
);
}
const variableValue = toLiteral(module.default);
const variable = t.variableDeclarator(
t.identifier(specifier.local.name),
variableValue
);
p.insertBefore({
type: "VariableDeclaration",
kind: "const",
declarations: [variable],
});
} else if (
specifier.type === "ImportSpecifier" &&
specifier.imported &&
specifier.imported.type === "Identifier" &&
specifier.local &&
specifier.local.type === "Identifier"
) {
if (!(specifier.imported.name in module)) {
throw new Error(
"babel-plugin-inline-constants: cannot access `" +
specifier.imported.name +
"` from `" +
p.node.source.value +
"`"
);
}
const variableValue = toLiteral(module[specifier.imported.name]);
const variable = t.variableDeclarator(
t.identifier(specifier.local.name),
variableValue
);
p.insertBefore({
type: "VariableDeclaration",
kind: "const",
declarations: [variable],
});
} else {
throw new Error("Cannot handle specifier `" + specifier.type + "`");
}
}
p.remove();
};
return {
visitor: {
ImportDeclaration: importDeclaration,
},
};
};

View File

@@ -5,8 +5,6 @@ const paths = require("./paths.js");
// Files from NPM Packages that should not be imported // Files from NPM Packages that should not be imported
module.exports.ignorePackages = ({ latestBuild }) => [ module.exports.ignorePackages = ({ latestBuild }) => [
// Bloats bundle and it's not used.
path.resolve(require.resolve("moment"), "../locale"),
// Part of yaml.js and only used for !!js functions that we don't use // Part of yaml.js and only used for !!js functions that we don't use
require.resolve("esprima"), require.resolve("esprima"),
]; ];
@@ -20,7 +18,8 @@ module.exports.emptyPackages = ({ latestBuild }) =>
require.resolve("@polymer/paper-styles/default-theme.js"), require.resolve("@polymer/paper-styles/default-theme.js"),
// Loads stuff from a CDN // Loads stuff from a CDN
require.resolve("@polymer/font-roboto/roboto.js"), require.resolve("@polymer/font-roboto/roboto.js"),
require.resolve("@vaadin/vaadin-material-styles/font-roboto.js"), require.resolve("@vaadin/vaadin-material-styles/typography.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Compatibility not needed for latest builds // Compatibility not needed for latest builds
latestBuild && latestBuild &&
// wrapped in require.resolve so it blows up if file no longer exists // wrapped in require.resolve so it blows up if file no longer exists
@@ -58,12 +57,23 @@ module.exports.babelOptions = ({ latestBuild }) => ({
"@babel/preset-env", "@babel/preset-env",
{ {
useBuiltIns: "entry", useBuiltIns: "entry",
corejs: "3.6", corejs: "3.15",
bugfixes: true,
}, },
], ],
"@babel/preset-typescript", "@babel/preset-typescript",
].filter(Boolean), ].filter(Boolean),
plugins: [ plugins: [
[
path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/inline-constants-plugin.js"
),
{
modules: ["@mdi/js"],
ignoreModuleNotFound: true,
},
],
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2}) // Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
!latestBuild && [ !latestBuild && [
"@babel/plugin-proposal-object-rest-spread", "@babel/plugin-proposal-object-rest-spread",
@@ -76,8 +86,14 @@ module.exports.babelOptions = ({ latestBuild }) => ({
"@babel/plugin-proposal-nullish-coalescing-operator", "@babel/plugin-proposal-nullish-coalescing-operator",
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }], ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
["@babel/plugin-proposal-private-methods", { loose: true }], ["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
["@babel/plugin-proposal-class-properties", { loose: true }], ["@babel/plugin-proposal-class-properties", { loose: true }],
].filter(Boolean), ].filter(Boolean),
exclude: [
// \\ for Windows, / for Mac OS and Linux
/node_modules[\\/]core-js/,
/node_modules[\\/]webpack[\\/]buildin/,
],
}); });
const outputPath = (outputRoot, latestBuild) => const outputPath = (outputRoot, latestBuild) =>

View File

@@ -47,8 +47,8 @@ gulp.task(
gulp.parallel("gen-icons-json", "build-translations"), gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-app", "copy-static-app",
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
...// Don't compress running tests // Don't compress running tests
(env.isTest() ? [] : ["compress-app"]), ...(env.isTest() ? [] : ["compress-app"]),
gulp.parallel( gulp.parallel(
"gen-pages-prod", "gen-pages-prod",
"gen-index-app-prod", "gen-index-app-prod",

View File

@@ -302,15 +302,23 @@ gulp.task("gen-index-hassio-prod", async () => {
function writeHassioEntrypoint(latestEntrypoint, es5Entrypoint) { function writeHassioEntrypoint(latestEntrypoint, es5Entrypoint) {
fs.mkdirSync(paths.hassio_output_root, { recursive: true }); fs.mkdirSync(paths.hassio_output_root, { recursive: true });
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
fs.writeFileSync( fs.writeFileSync(
path.resolve(paths.hassio_output_root, "entrypoint.js"), path.resolve(paths.hassio_output_root, "entrypoint.js"),
` `
try { function loadES5() {
new Function("import('${latestEntrypoint}')")();
} catch (err) {
var el = document.createElement('script'); var el = document.createElement('script');
el.src = '${es5Entrypoint}'; el.src = '${es5Entrypoint}';
document.body.appendChild(el); document.body.appendChild(el);
}
if (/.*Version\\/(?:11|12)(?:\\.\\d+)*.*Safari\\//.test(navigator.userAgent)) {
loadES5();
} else {
try {
new Function("import('${latestEntrypoint}')")();
} catch (err) {
loadES5();
}
} }
`, `,
{ encoding: "utf-8" } { encoding: "utf-8" }

View File

@@ -2,7 +2,6 @@
const gulp = require("gulp"); const gulp = require("gulp");
const path = require("path"); const path = require("path");
const cpx = require("cpx");
const fs = require("fs-extra"); const fs = require("fs-extra");
const paths = require("../paths"); const paths = require("../paths");
@@ -62,9 +61,12 @@ function copyLoaderJS(staticDir) {
function copyFonts(staticDir) { function copyFonts(staticDir) {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
// Local fonts // Local fonts
cpx.copySync( fs.copySync(
npmPath("roboto-fontface/fonts/roboto/*.woff2"), npmPath("roboto-fontface/fonts/roboto/"),
staticPath("fonts/roboto") staticPath("fonts/roboto/"),
{
filter: (src) => !src.includes(".") || src.endsWith(".woff2"),
}
); );
} }

View File

@@ -19,10 +19,12 @@ const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: false }), createConfigFunc({ ...params, latestBuild: false }),
]; ];
const isWsl = fs const isWsl =
.readFileSync("/proc/version", "utf-8") fs.existsSync("/proc/version") &&
.toLocaleLowerCase() fs
.includes("microsoft"); .readFileSync("/proc/version", "utf-8")
.toLocaleLowerCase()
.includes("microsoft");
/** /**
* @param {{ * @param {{
@@ -84,8 +86,15 @@ const prodBuild = (conf) =>
gulp.task("webpack-watch-app", () => { gulp.task("webpack-watch-app", () => {
// This command will run forever because we don't close compiler // This command will run forever because we don't close compiler
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch( webpack(
{ ignored: /build-translations/, poll: isWsl }, process.env.ES5
? bothBuilds(createAppConfig, { isProdBuild: false })
: createAppConfig({ isProdBuild: false, latestBuild: true })
).watch(
{
ignored: /build-translations/,
poll: isWsl,
},
doneHandler() doneHandler()
); );
gulp.watch( gulp.watch(

View File

@@ -49,12 +49,16 @@ const createWebpackConfig = ({
test: /\.m?js$|\.ts$/, test: /\.m?js$|\.ts$/,
use: { use: {
loader: "babel-loader", loader: "babel-loader",
options: bundle.babelOptions({ latestBuild }), options: {
...bundle.babelOptions({ latestBuild }),
cacheDirectory: !isProdBuild,
cacheCompression: false,
},
}, },
}, },
{ {
test: /\.css$/, test: /\.css$/,
use: "raw-loader", type: "asset/source",
}, },
], ],
}, },
@@ -66,6 +70,8 @@ const createWebpackConfig = ({
terserOptions: bundle.terserOptions(latestBuild), terserOptions: bundle.terserOptions(latestBuild),
}), }),
], ],
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
}, },
plugins: [ plugins: [
new WebpackManifestPlugin({ new WebpackManifestPlugin({
@@ -112,16 +118,6 @@ const createWebpackConfig = ({
new RegExp(bundle.emptyPackages({ latestBuild }).join("|")), new RegExp(bundle.emptyPackages({ latestBuild }).join("|")),
path.resolve(paths.polymer_dir, "src/util/empty.js") path.resolve(paths.polymer_dir, "src/util/empty.js")
), ),
// We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one
new webpack.NormalModuleReplacementPlugin(
new RegExp(
path.resolve(
paths.polymer_dir,
"src/resources/lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
)
),
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")
),
!isProdBuild && new LogStartCompilePlugin(), !isProdBuild && new LogStartCompilePlugin(),
].filter(Boolean), ].filter(Boolean),
resolve: { resolve: {
@@ -134,15 +130,13 @@ const createWebpackConfig = ({
}, },
output: { output: {
filename: ({ chunk }) => { filename: ({ chunk }) => {
if (!isProdBuild || dontHash.has(chunk.name)) { if (!isProdBuild || isStatsBuild || dontHash.has(chunk.name)) {
return `${chunk.name}.js`; return `${chunk.name}.js`;
} }
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`; return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
}, },
chunkFilename: chunkFilename:
isProdBuild && !isStatsBuild isProdBuild && !isStatsBuild ? "[chunkhash:8].js" : "[id].chunk.js",
? "chunk.[chunkhash].js"
: "[name].chunk.js",
path: outputPath, path: outputPath,
publicPath, publicPath,
// To silence warning in worker plugin // To silence warning in worker plugin

View File

@@ -139,7 +139,7 @@
Your authentication credentials or Home Assistant url are never sent Your authentication credentials or Home Assistant url are never sent
to the Cloud. You can validate this behavior in to the Cloud. You can validate this behavior in
<a <a
href="https://github.com/home-assistant/home-assistant-polymer/tree/dev/cast" href="https://github.com/home-assistant/frontend/tree/dev/cast"
target="_blank" target="_blank"
>the source code</a >the source code</a
>. >.

View File

@@ -5,8 +5,8 @@ import {
import { castContext } from "../cast_context"; import { castContext } from "../cast_context";
export const castDemoLovelace: () => LovelaceConfig = () => { export const castDemoLovelace: () => LovelaceConfig = () => {
const touchSupported = castContext.getDeviceCapabilities() const touchSupported =
.touch_input_supported; castContext.getDeviceCapabilities().touch_input_supported;
return { return {
views: [ views: [
{ {

View File

@@ -29,6 +29,11 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
}, },
], ],
}, },
{
title: "Energy distribution today",
type: "energy-distribution",
link_dashboard: true,
},
{ {
type: "thermostat", type: "thermostat",
entity: "climate.upstairs", entity: "climate.upstairs",
@@ -113,8 +118,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
"brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {
@@ -196,8 +200,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
"brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {
@@ -277,8 +280,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
"brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {
@@ -315,8 +317,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
on: "/assets/arsaboo/icons/light_bulb_on.png", on: "/assets/arsaboo/icons/light_bulb_on.png",
}, },
state_filter: { state_filter: {
on: on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
"brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)", off: "brightness(80%) saturate(0.8)",
}, },
style: { style: {

View File

@@ -1,5 +1,6 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { Lovelace } from "../../../src/panels/lovelace/types"; import { Lovelace } from "../../../src/panels/lovelace/types";
import { energyEntities } from "../stubs/entities";
import { DemoConfig } from "./types"; import { DemoConfig } from "./types";
export const demoConfigs: Array<() => Promise<DemoConfig>> = [ export const demoConfigs: Array<() => Promise<DemoConfig>> = [
@@ -12,9 +13,8 @@ export const demoConfigs: Array<() => Promise<DemoConfig>> = [
// eslint-disable-next-line import/no-mutable-exports // eslint-disable-next-line import/no-mutable-exports
export let selectedDemoConfigIndex = 0; export let selectedDemoConfigIndex = 0;
// eslint-disable-next-line import/no-mutable-exports // eslint-disable-next-line import/no-mutable-exports
export let selectedDemoConfig: Promise<DemoConfig> = demoConfigs[ export let selectedDemoConfig: Promise<DemoConfig> =
selectedDemoConfigIndex demoConfigs[selectedDemoConfigIndex]();
]();
export const setDemoConfig = async ( export const setDemoConfig = async (
hass: MockHomeAssistant, hass: MockHomeAssistant,
@@ -28,6 +28,7 @@ export const setDemoConfig = async (
selectedDemoConfig = confProm; selectedDemoConfig = confProm;
hass.addEntities(config.entities(hass.localize), true); hass.addEntities(config.entities(hass.localize), true);
hass.addEntities(energyEntities());
lovelace.saveConfig(config.lovelace(hass.localize)); lovelace.saveConfig(config.lovelace(hass.localize));
hass.mockTheme(config.theme()); hass.mockTheme(config.theme());
}; };

View File

@@ -980,8 +980,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
icon: "mdi:account-off", icon: "mdi:account-off",
custom_ui_state_card: "state-card-custom-ui", custom_ui_state_card: "state-card-custom-ui",
templates: { templates: {
icon: icon: "if (state === 'on') return 'mdi:account'; else if (state === 'off') return 'mdi:account-off';\n",
"if (state === 'on') return 'mdi:account'; else if (state === 'off') return 'mdi:account-off';\n",
icon_color: icon_color:
"if (state === 'on') return 'rgb(56, 150, 56)'; else if (state === 'off') return 'rgb(249, 251, 255)';\n", "if (state === 'on') return 'rgb(56, 150, 56)'; else if (state === 'off') return 'rgb(249, 251, 255)';\n",
}, },
@@ -1005,8 +1004,7 @@ export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
icon: "mdi:account-multiple-minus", icon: "mdi:account-multiple-minus",
custom_ui_state_card: "state-card-custom-ui", custom_ui_state_card: "state-card-custom-ui",
templates: { templates: {
icon: icon: "if (state === 'on') return 'mdi:account-group'; else if (state === 'off') return 'mdi:account-multiple-minus';\n",
"if (state === 'on') return 'mdi:account-group'; else if (state === 'off') return 'mdi:account-multiple-minus';\n",
icon_color: icon_color:
"if (state === 'on') return 'rgb(56, 150, 56)'; else if (state === 'off') return 'rgb(249, 251, 255)';\n", "if (state === 'on') return 'rgb(56, 150, 56)'; else if (state === 'off') return 'rgb(249, 251, 255)';\n",
}, },

View File

@@ -19,7 +19,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass!: MockHomeAssistant; @property({ attribute: false }) public hass!: MockHomeAssistant;
@state() private _switching?: boolean; @state() private _switching = false;
private _hidden = localStorage.hide_demo_card; private _hidden = localStorage.hide_demo_card;
@@ -27,12 +27,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
return this._hidden ? 0 : 2; return this._hidden ? 0 : 2;
} }
public setConfig( public setConfig(_config: LovelaceCardConfig) {}
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
config: LovelaceCardConfig
// eslint-disable-next-line @typescript-eslint/no-empty-function
) {}
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._hidden) { if (this._hidden) {

View File

@@ -1,5 +1,3 @@
import "@polymer/polymer/lib/elements/dom-if";
import "@polymer/polymer/lib/elements/dom-repeat";
import "../../src/resources/ha-style"; import "../../src/resources/ha-style";
import "../../src/resources/roboto"; import "../../src/resources/roboto";
import "../../src/resources/safari-14-attachshadow-patch"; import "../../src/resources/safari-14-attachshadow-patch";

View File

@@ -20,6 +20,10 @@ import { mockShoppingList } from "./stubs/shopping_list";
import { mockSystemLog } from "./stubs/system_log"; import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template"; import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations"; import { mockTranslations } from "./stubs/translations";
import { mockEnergy } from "./stubs/energy";
import { mockConfig } from "./stubs/config";
import { energyEntities } from "./stubs/entities";
import { mockForecastSolar } from "./stubs/forecast_solar";
class HaDemo extends HomeAssistantAppEl { class HaDemo extends HomeAssistantAppEl {
protected async _initializeHass() { protected async _initializeHass() {
@@ -47,8 +51,13 @@ class HaDemo extends HomeAssistantAppEl {
mockEvents(hass); mockEvents(hass);
mockMediaPlayer(hass); mockMediaPlayer(hass);
mockFrontend(hass); mockFrontend(hass);
mockEnergy(hass);
mockForecastSolar(hass);
mockConfig(hass);
mockPersistentNotification(hass); mockPersistentNotification(hass);
hass.addEntities(energyEntities());
// Once config is loaded AND localize, set entities and apply theme. // Once config is loaded AND localize, set entities and apply theme.
Promise.all([selectedDemoConfig, localizePromise]).then( Promise.all([selectedDemoConfig, localizePromise]).then(
([conf, localize]) => { ([conf, localize]) => {

41
demo/src/stubs/config.ts Normal file
View File

@@ -0,0 +1,41 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfig = (hass: MockHomeAssistant) => {
hass.mockAPI("config/config_entries/entry", () => [
{
entry_id: "co2signal",
domain: "co2signal",
title: "CO2 Signal",
source: "user",
state: "loaded",
supports_options: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
},
]);
hass.mockWS("config/entity_registry/list", () => [
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.co2_intensity",
name: null,
icon: null,
platform: "co2signal",
},
{
config_entry_id: "co2signal",
device_id: "co2signal",
area_id: null,
disabled_by: null,
entity_id: "sensor.grid_fossil_fuel_percentage",
name: null,
icon: null,
platform: "co2signal",
},
]);
};

70
demo/src/stubs/energy.ts Normal file
View File

@@ -0,0 +1,70 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEnergy = (hass: MockHomeAssistant) => {
hass.mockWS("energy/get_prefs", () => ({
energy_sources: [
{
type: "grid",
flow_from: [
{
stat_energy_from: "sensor.energy_consumption_tarif_1",
stat_cost: "sensor.energy_consumption_tarif_1_cost",
entity_energy_from: "sensor.energy_consumption_tarif_1",
entity_energy_price: null,
number_energy_price: null,
},
{
stat_energy_from: "sensor.energy_consumption_tarif_2",
stat_cost: "sensor.energy_consumption_tarif_2_cost",
entity_energy_from: "sensor.energy_consumption_tarif_2",
entity_energy_price: null,
number_energy_price: null,
},
],
flow_to: [
{
stat_energy_to: "sensor.energy_production_tarif_1",
stat_compensation: "sensor.energy_production_tarif_1_compensation",
entity_energy_to: "sensor.energy_production_tarif_1",
entity_energy_price: null,
number_energy_price: null,
},
{
stat_energy_to: "sensor.energy_production_tarif_2",
stat_compensation: "sensor.energy_production_tarif_2_compensation",
entity_energy_to: "sensor.energy_production_tarif_2",
entity_energy_price: null,
number_energy_price: null,
},
],
cost_adjustment_day: 0,
},
{
type: "solar",
stat_energy_from: "sensor.solar_production",
config_entry_solar_forecast: ["solar_forecast"],
},
],
device_consumption: [
{
stat_consumption: "sensor.energy_car",
},
{
stat_consumption: "sensor.energy_ac",
},
{
stat_consumption: "sensor.energy_washing_machine",
},
{
stat_consumption: "sensor.energy_dryer",
},
{
stat_consumption: "sensor.energy_heat_pump",
},
{
stat_consumption: "sensor.energy_boiler",
},
],
}));
hass.mockWS("energy/info", () => ({ cost_sensors: [] }));
};

143
demo/src/stubs/entities.ts Normal file
View File

@@ -0,0 +1,143 @@
import { convertEntities } from "../../../src/fake_data/entity";
export const energyEntities = () =>
convertEntities({
"sensor.grid_fossil_fuel_percentage": {
entity_id: "sensor.grid_fossil_fuel_percentage",
state: "88.6",
attributes: {
unit_of_measurement: "%",
},
},
"sensor.solar_production": {
entity_id: "sensor.solar_production",
state: "88.6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Solar",
unit_of_measurement: "kWh",
},
},
"sensor.energy_consumption_tarif_1": {
entity_id: "sensor.energy_consumption_tarif_1 ",
state: "88.6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Grid consumption low tariff",
unit_of_measurement: "kWh",
},
},
"sensor.energy_consumption_tarif_2": {
entity_id: "sensor.energy_consumption_tarif_2",
state: "88.6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Grid consumption high tariff",
unit_of_measurement: "kWh",
},
},
"sensor.energy_production_tarif_1": {
entity_id: "sensor.energy_production_tarif_1",
state: "88.6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Returned to grid low tariff",
unit_of_measurement: "kWh",
},
},
"sensor.energy_production_tarif_2": {
entity_id: "sensor.energy_production_tarif_2",
state: "88.6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Returned to grid high tariff",
unit_of_measurement: "kWh",
},
},
"sensor.energy_consumption_tarif_1_cost": {
entity_id: "sensor.energy_consumption_tarif_1_cost",
state: "2",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
unit_of_measurement: "EUR",
},
},
"sensor.energy_consumption_tarif_2_cost": {
entity_id: "sensor.energy_consumption_tarif_2_cost",
state: "2",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
unit_of_measurement: "EUR",
},
},
"sensor.energy_production_tarif_1_compensation": {
entity_id: "sensor.energy_production_tarif_1_compensation",
state: "2",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
unit_of_measurement: "EUR",
},
},
"sensor.energy_production_tarif_2_compensation": {
entity_id: "sensor.energy_production_tarif_2_compensation",
state: "2",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
unit_of_measurement: "EUR",
},
},
"sensor.energy_car": {
entity_id: "sensor.energy_car",
state: "4",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Electric car",
unit_of_measurement: "kWh",
},
},
"sensor.energy_ac": {
entity_id: "sensor.energy_ac",
state: "3",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Air conditioning",
unit_of_measurement: "kWh",
},
},
"sensor.energy_washing_machine": {
entity_id: "sensor.energy_washing_machine",
state: "6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Washing machine",
unit_of_measurement: "kWh",
},
},
"sensor.energy_dryer": {
entity_id: "sensor.energy_dryer",
state: "5.5",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Dryer",
unit_of_measurement: "kWh",
},
},
"sensor.energy_heat_pump": {
entity_id: "sensor.energy_heat_pump",
state: "6",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Heat pump",
unit_of_measurement: "kWh",
},
},
"sensor.energy_boiler": {
entity_id: "sensor.energy_boiler",
state: "7",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Boiler",
unit_of_measurement: "kWh",
},
},
});

View File

@@ -0,0 +1,55 @@
import { format, startOfToday, startOfTomorrow } from "date-fns";
import { ForecastSolarForecast } from "../../../src/data/forecast_solar";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockForecastSolar = (hass: MockHomeAssistant) => {
const todayString = format(startOfToday(), "yyyy-MM-dd");
const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd");
hass.mockWS(
"forecast_solar/forecasts",
(): Record<string, ForecastSolarForecast> => ({
solar_forecast: {
wh_hours: {
[`${todayString}T06:00:00`]: 0,
[`${todayString}T06:23:00`]: 6,
[`${todayString}T06:45:00`]: 39,
[`${todayString}T07:00:00`]: 28,
[`${todayString}T08:00:00`]: 208,
[`${todayString}T09:00:00`]: 352,
[`${todayString}T10:00:00`]: 544,
[`${todayString}T11:00:00`]: 748,
[`${todayString}T12:00:00`]: 1259,
[`${todayString}T13:00:00`]: 1361,
[`${todayString}T14:00:00`]: 1373,
[`${todayString}T15:00:00`]: 1370,
[`${todayString}T16:00:00`]: 1186,
[`${todayString}T17:00:00`]: 937,
[`${todayString}T18:00:00`]: 652,
[`${todayString}T19:00:00`]: 370,
[`${todayString}T20:00:00`]: 155,
[`${todayString}T21:48:00`]: 24,
[`${todayString}T22:36:00`]: 0,
[`${tomorrowString}T06:01:00`]: 0,
[`${tomorrowString}T06:23:00`]: 9,
[`${tomorrowString}T06:45:00`]: 47,
[`${tomorrowString}T07:00:00`]: 48,
[`${tomorrowString}T08:00:00`]: 473,
[`${tomorrowString}T09:00:00`]: 827,
[`${tomorrowString}T10:00:00`]: 1153,
[`${tomorrowString}T11:00:00`]: 1413,
[`${tomorrowString}T12:00:00`]: 1590,
[`${tomorrowString}T13:00:00`]: 1652,
[`${tomorrowString}T14:00:00`]: 1612,
[`${tomorrowString}T15:00:00`]: 1438,
[`${tomorrowString}T16:00:00`]: 1149,
[`${tomorrowString}T17:00:00`]: 830,
[`${tomorrowString}T18:00:00`]: 542,
[`${tomorrowString}T19:00:00`]: 311,
[`${tomorrowString}T20:00:00`]: 140,
[`${tomorrowString}T21:47:00`]: 22,
[`${tomorrowString}T22:34:00`]: 0,
},
},
})
);
};

View File

@@ -1,4 +1,6 @@
import { addHours, differenceInHours } from "date-fns";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { StatisticValue } from "../../../src/data/history";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
interface HistoryQueryParams { interface HistoryQueryParams {
@@ -64,17 +66,215 @@ const generateHistory = (state, deltas) => {
const incrementalUnits = ["clients", "queries", "ads"]; const incrementalUnits = ["clients", "queries", "ads"];
const generateMeanStatistics = (
id: string,
start: Date,
end: Date,
initValue: number,
maxDiff: number
) => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let lastVal = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const delta = Math.random() * maxDiff;
const mean = lastVal + delta;
statistics.push({
statistic_id: id,
start: currentDate.toISOString(),
mean,
min: mean - Math.random() * maxDiff,
max: mean + Math.random() * maxDiff,
last_reset: "1970-01-01T00:00:00+00:00",
state: mean,
sum: null,
});
lastVal = mean;
currentDate = addHours(currentDate, 1);
}
return statistics;
};
const generateSumStatistics = (
id: string,
start: Date,
end: Date,
initValue: number,
maxDiff: number
) => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let sum = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const add = Math.random() * maxDiff;
sum += add;
statistics.push({
statistic_id: id,
start: currentDate.toISOString(),
mean: null,
min: null,
max: null,
last_reset: "1970-01-01T00:00:00+00:00",
state: initValue + sum,
sum,
});
currentDate = addHours(currentDate, 1);
}
return statistics;
};
const generateCurvedStatistics = (
id: string,
start: Date,
end: Date,
initValue: number,
maxDiff: number,
metered: boolean
) => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let sum = initValue;
const hours = differenceInHours(end, start) - 1;
let i = 0;
let half = false;
const now = new Date();
while (end > currentDate && currentDate < now) {
const add = Math.random() * maxDiff;
sum += i * add;
statistics.push({
statistic_id: id,
start: currentDate.toISOString(),
mean: null,
min: null,
max: null,
last_reset: "1970-01-01T00:00:00+00:00",
state: initValue + sum,
sum: metered ? sum : null,
});
currentDate = addHours(currentDate, 1);
if (!half && i > hours / 2) {
half = true;
}
i += half ? -1 : 1;
}
return statistics;
};
const statisticsFunctions: Record<
string,
(id: string, start: Date, end: Date) => StatisticValue[]
> = {
"sensor.energy_consumption_tarif_1": (id: string, start: Date, end: Date) => {
const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000);
const morningLow = generateSumStatistics(id, start, morningEnd, 0, 0.7);
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
const morningFinalVal = morningLow.length
? morningLow[morningLow.length - 1].sum!
: 0;
const empty = generateSumStatistics(
id,
morningEnd,
eveningStart,
morningFinalVal,
0
);
const eveningLow = generateSumStatistics(
id,
eveningStart,
end,
morningFinalVal,
0.7
);
return [...morningLow, ...empty, ...eveningLow];
},
"sensor.energy_consumption_tarif_2": (id: string, start: Date, end: Date) => {
const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
const highTarif = generateSumStatistics(
id,
morningEnd,
eveningStart,
0,
0.3
);
const highTarifFinalVal = highTarif.length
? highTarif[highTarif.length - 1].sum!
: 0;
const morning = generateSumStatistics(id, start, morningEnd, 0, 0);
const evening = generateSumStatistics(
id,
eveningStart,
end,
highTarifFinalVal,
0
);
return [...morning, ...highTarif, ...evening];
},
"sensor.energy_production_tarif_1": (id, start, end) =>
generateSumStatistics(id, start, end, 0, 0),
"sensor.energy_production_tarif_1_compensation": (id, start, end) =>
generateSumStatistics(id, start, end, 0, 0),
"sensor.energy_production_tarif_2": (id, start, end) => {
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
const production = generateCurvedStatistics(
id,
productionStart,
productionEnd,
0,
0.15,
true
);
const productionFinalVal = production.length
? production[production.length - 1].sum!
: 0;
const morning = generateSumStatistics(id, start, productionStart, 0, 0);
const evening = generateSumStatistics(
id,
productionEnd,
end,
productionFinalVal,
0
);
return [...morning, ...production, ...evening];
},
"sensor.solar_production": (id, start, end) => {
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
const production = generateCurvedStatistics(
id,
productionStart,
productionEnd,
0,
0.3,
true
);
const productionFinalVal = production.length
? production[production.length - 1].sum!
: 0;
const morning = generateSumStatistics(id, start, productionStart, 0, 0);
const evening = generateSumStatistics(
id,
productionEnd,
end,
productionFinalVal,
0
);
return [...morning, ...production, ...evening];
},
"sensor.grid_fossil_fuel_percentage": (id, start, end) =>
generateMeanStatistics(id, start, end, 35, 1.3),
};
export const mockHistory = (mockHass: MockHomeAssistant) => { export const mockHistory = (mockHass: MockHomeAssistant) => {
mockHass.mockAPI( mockHass.mockAPI(
new RegExp("history/period/.+"), new RegExp("history/period/.+"),
( (hass, _method, path, _parameters) => {
hass,
// @ts-ignore
method,
path,
// @ts-ignore
parameters
) => {
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]); const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
const entities = params.filter_entity_id.split(","); const entities = params.filter_entity_id.split(",");
@@ -95,7 +295,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
const numberState = Number(state.state); const numberState = Number(state.state);
if (isNaN(numberState)) { if (isNaN(numberState)) {
// eslint-disable-next-line // eslint-disable-next-line no-console
console.log( console.log(
"Ignoring state with unparsable state but with a unit", "Ignoring state with unparsable state but with a unit",
entityId, entityId,
@@ -140,4 +340,40 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
return results; return results;
} }
); );
mockHass.mockWS("history/list_statistic_ids", () => []);
mockHass.mockWS(
"history/statistics_during_period",
({ statistic_ids, start_time, end_time }, hass) => {
const start = new Date(start_time);
const end = end_time ? new Date(end_time) : new Date();
const statistics: Record<string, StatisticValue[]> = {};
statistic_ids.forEach((id: string) => {
if (id in statisticsFunctions) {
statistics[id] = statisticsFunctions[id](id, start, end);
} else {
const entityState = hass.states[id];
const state = entityState ? Number(entityState.state) : 1;
statistics[id] =
entityState && "last_reset" in entityState.attributes
? generateSumStatistics(
id,
start,
end,
state,
state * (state > 80 ? 0.01 : 0.05)
)
: generateMeanStatistics(
id,
start,
end,
state,
state * (state > 80 ? 0.05 : 0.1)
);
}
});
return statistics;
}
);
}; };

View File

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

View File

@@ -6,7 +6,7 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
body: { message: "Template dev tool does not work in the demo." }, body: { message: "Template dev tool does not work in the demo." },
}) })
); );
hass.mockWS("render_template", (msg, onChange) => { hass.mockWS("render_template", (msg, _hass, onChange) => {
onChange!({ onChange!({
result: msg.template, result: msg.template,
listeners: { all: false, domains: [], entities: [], time: false }, listeners: { all: false, domains: [], entities: [], time: false },

View File

@@ -2,12 +2,12 @@ import { html, css, LitElement, TemplateResult } from "lit";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/trace/hat-script-graph"; import "../../../src/components/trace/hat-script-graph";
import "../../../src/components/trace/hat-trace-timeline"; import "../../../src/components/trace/hat-trace-timeline";
import { customElement, property, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass"; import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { DemoTrace } from "../data/traces/types"; import { DemoTrace } from "../data/traces/types";
import { basicTrace } from "../data/traces/basic_trace"; import { basicTrace } from "../data/traces/basic_trace";
import { motionLightTrace } from "../data/traces/motion-light-trace"; import { motionLightTrace } from "../data/traces/motion-light-trace";
import { customElement, property, state } from "lit/decorators";
const traces: DemoTrace[] = [basicTrace, motionLightTrace]; const traces: DemoTrace[] = [basicTrace, motionLightTrace];

View File

@@ -2,6 +2,8 @@ import { html, css, LitElement, TemplateResult } from "lit";
import "../../../src/components/ha-formfield"; import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch"; import "../../../src/components/ha-switch";
import { classMap } from "lit/directives/class-map";
import { customElement, property, state } from "lit/decorators";
import { IntegrationManifest } from "../../../src/data/integration"; import { IntegrationManifest } from "../../../src/data/integration";
import { provideHass } from "../../../src/fake_data/provide_hass"; import { provideHass } from "../../../src/fake_data/provide_hass";
@@ -15,8 +17,6 @@ import type {
} from "../../../src/panels/config/integrations/ha-config-integrations"; } from "../../../src/panels/config/integrations/ha-config-integrations";
import { DeviceRegistryEntry } from "../../../src/data/device_registry"; import { DeviceRegistryEntry } from "../../../src/data/device_registry";
import { EntityRegistryEntry } from "../../../src/data/entity_registry"; import { EntityRegistryEntry } from "../../../src/data/entity_registry";
import { classMap } from "lit/directives/class-map";
import { customElement, property, state } from "lit/decorators";
const createConfigEntry = ( const createConfigEntry = (
title: string, title: string,

View File

@@ -2,7 +2,6 @@ import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list"; import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
import { DEFAULT_SCHEMA, Type } from "js-yaml"; import { DEFAULT_SCHEMA, Type } from "js-yaml";
import { import {
css, css,
@@ -134,7 +133,7 @@ class HassioAddonConfig extends LitElement {
></ha-form>` ></ha-form>`
: html` <ha-yaml-editor : html` <ha-yaml-editor
@value-changed=${this._configChanged} @value-changed=${this._configChanged}
.schema=${ADDON_YAML_SCHEMA} .yamlSchema=${ADDON_YAML_SCHEMA}
></ha-yaml-editor>`} ></ha-yaml-editor>`}
${this._error ? html` <div class="errors">${this._error}</div> ` : ""} ${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${!this._yamlMode || ${!this._yamlMode ||
@@ -269,6 +268,9 @@ class HassioAddonConfig extends LitElement {
private async _saveTapped(ev: CustomEvent): Promise<void> { private async _saveTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any; const button = ev.currentTarget as any;
const options: Record<string, unknown> = this._yamlMode
? this._editor?.value
: this._options;
const eventdata = { const eventdata = {
success: true, success: true,
response: undefined, response: undefined,
@@ -282,13 +284,13 @@ class HassioAddonConfig extends LitElement {
const validation = await validateHassioAddonOption( const validation = await validateHassioAddonOption(
this.hass, this.hass,
this.addon.slug, this.addon.slug,
this._editor?.value options
); );
if (!validation.valid) { if (!validation.valid) {
throw Error(validation.message); throw Error(validation.message);
} }
await setHassioAddonOption(this.hass, this.addon.slug, { await setHassioAddonOption(this.hass, this.addon.slug, {
options: this._yamlMode ? this._editor?.value : this._options, options,
}); });
this._configHasChanged = false; this._configHasChanged = false;
@@ -326,10 +328,6 @@ class HassioAddonConfig extends LitElement {
color: var(--error-color); color: var(--error-color);
margin-top: 16px; margin-top: 16px;
} }
iron-autogrow-textarea {
width: 100%;
font-family: var(--code-font-family, monospace);
}
.syntaxerror { .syntaxerror {
color: var(--error-color); color: var(--error-color);
} }

View File

@@ -2,6 +2,7 @@ import "../../../../src/components/ha-card";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
import { customElement, property, state } from "lit/decorators";
import { import {
fetchHassioAddonDocumentation, fetchHassioAddonDocumentation,
HassioAddonDetails, HassioAddonDetails,
@@ -12,7 +13,6 @@ import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { customElement, property, state } from "lit/decorators";
@customElement("hassio-addon-documentation-tab") @customElement("hassio-addon-documentation-tab")
class HassioAddonDocumentationDashboard extends LitElement { class HassioAddonDocumentationDashboard extends LitElement {

View File

@@ -892,10 +892,19 @@ class HassioAddonInfo extends LitElement {
private async _openChangelog(): Promise<void> { private async _openChangelog(): Promise<void> {
try { try {
const content = await fetchHassioAddonChangelog( let content = await fetchHassioAddonChangelog(this.hass, this.addon.slug);
this.hass, if (
this.addon.slug content.includes(`# ${this.addon.version}`) &&
); content.includes(`# ${this.addon.version_latest}`)
) {
const newcontent = content.split(`# ${this.addon.version}`)[0];
if (newcontent.includes(`# ${this.addon.version_latest}`)) {
// Only change the content if the new version still exist
// if the changelog does not have the newests version on top
// this will not be true, and we don't modify the content
content = newcontent;
}
}
showHassioMarkdownDialog(this, { showHassioMarkdownDialog(this, {
title: this.supervisor.localize("addon.dashboard.changelog"), title: this.supervisor.localize("addon.dashboard.changelog"),
content, content,
@@ -977,7 +986,6 @@ class HassioAddonInfo extends LitElement {
showDialogSupervisorUpdate(this, { showDialogSupervisorUpdate(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
name: this.addon.name, name: this.addon.name,
slug: this.addon.slug,
version: this.addon.version_latest, version: this.addon.version_latest,
snapshotParams: { snapshotParams: {
name: `addon_${this.addon.slug}_${this.addon.version}`, name: `addon_${this.addon.slug}_${this.addon.version}`,

View File

@@ -1,6 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiFolderUpload } from "@mdi/js"; import { mdiFolderUpload } from "@mdi/js";
import "@polymer/iron-input/iron-input";
import "@polymer/paper-input/paper-input-container"; import "@polymer/paper-input/paper-input-container";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";

View File

@@ -86,7 +86,7 @@ export class HassioUpdate extends LitElement {
"hassio/supervisor/update", "hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}` `https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
)} )}
${this.supervisor.host.features.includes("hassos") ${this.supervisor.host.features.includes("haos")
? this._renderUpdateCard( ? this._renderUpdateCard(
"Operating System", "Operating System",
"os", "os",
@@ -161,7 +161,6 @@ export class HassioUpdate extends LitElement {
showDialogSupervisorUpdate(this, { showDialogSupervisorUpdate(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
name: "Home Assistant Core", name: "Home Assistant Core",
slug: "core",
version: this.supervisor.core.version_latest, version: this.supervisor.core.version_latest,
snapshotParams: { snapshotParams: {
name: `core_${this.supervisor.core.version}`, name: `core_${this.supervisor.core.version}`,

View File

@@ -61,10 +61,6 @@ class HassioMarkdownDialog extends LitElement {
app-toolbar [main-title] { app-toolbar [main-title] {
margin-left: 16px; margin-left: 16px;
} }
paper-checkbox {
display: block;
margin: 4px;
}
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
ha-paper-dialog { ha-paper-dialog {
max-height: 100%; max-height: 100%;

View File

@@ -41,7 +41,8 @@ const IP_VERSIONS = ["ipv4", "ipv6"];
@customElement("dialog-hassio-network") @customElement("dialog-hassio-network")
export class DialogHassioNetwork export class DialogHassioNetwork
extends LitElement extends LitElement
implements HassDialog<HassioNetworkDialogParams> { implements HassDialog<HassioNetworkDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor; @property({ attribute: false }) public supervisor!: Supervisor;
@@ -107,7 +108,7 @@ export class DialogHassioNetwork
</mwc-icon-button> </mwc-icon-button>
</ha-header-bar> </ha-header-bar>
${this._interfaces.length > 1 ${this._interfaces.length > 1
? html` <mwc-tab-bar ? html`<mwc-tab-bar
.activeIndex=${this._curTabIndex} .activeIndex=${this._curTabIndex}
@MDCTabBar:activated=${this._handleTabActivated} @MDCTabBar:activated=${this._handleTabActivated}
>${this._interfaces.map( >${this._interfaces.map(
@@ -492,7 +493,7 @@ export class DialogHassioNetwork
} }
private _handleRadioValueChangedAp(ev: CustomEvent): void { private _handleRadioValueChangedAp(ev: CustomEvent): void {
const value = ((ev.target as any).value as string) as const value = (ev.target as any).value as string as
| "open" | "open"
| "wep" | "wep"
| "wpa-psk"; | "wpa-psk";

View File

@@ -161,9 +161,9 @@ class HassioRegistriesDialog extends LitElement {
public focus(): void { public focus(): void {
this.updateComplete.then(() => this.updateComplete.then(() =>
(this.shadowRoot?.querySelector( (
"[dialogInitialFocus]" this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
) as HTMLElement)?.focus() )?.focus()
); );
} }

View File

@@ -161,9 +161,9 @@ class HassioRepositoriesDialog extends LitElement {
public focus() { public focus() {
this.updateComplete.then(() => this.updateComplete.then(() =>
(this.shadowRoot?.querySelector( (
"[dialogInitialFocus]" this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
) as HTMLElement)?.focus() )?.focus()
); );
} }

View File

@@ -12,7 +12,8 @@ import { HassioSnapshotUploadDialogParams } from "./show-dialog-snapshot-upload"
@customElement("dialog-hassio-snapshot-upload") @customElement("dialog-hassio-snapshot-upload")
export class DialogHassioSnapshotUpload export class DialogHassioSnapshotUpload
extends LitElement extends LitElement
implements HassDialog<HassioSnapshotUploadDialogParams> { implements HassDialog<HassioSnapshotUploadDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: HassioSnapshotUploadDialogParams; @state() private _params?: HassioSnapshotUploadDialogParams;

View File

@@ -30,7 +30,8 @@ import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
@customElement("dialog-hassio-snapshot") @customElement("dialog-hassio-snapshot")
class HassioSnapshotDialog class HassioSnapshotDialog
extends LitElement extends LitElement
implements HassDialog<HassioSnapshotDialogParams> { implements HassDialog<HassioSnapshotDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string; @state() private _error?: string;
@@ -297,8 +298,7 @@ class HassioSnapshotDialog
if (window.location.href.includes("ui.nabu.casa")) { if (window.location.href.includes("ui.nabu.casa")) {
const confirm = await showConfirmationDialog(this, { const confirm = await showConfirmationDialog(this, {
title: "Potential slow download", title: "Potential slow download",
text: text: "Downloading snapshots over the Nabu Casa URL will take some time, it is recomended to use your local URL instead, do you want to continue?",
"Downloading snapshots over the Nabu Casa URL will take some time, it is recomended to use your local URL instead, do you want to continue?",
confirmText: "continue", confirmText: "continue",
dismissText: "cancel", dismissText: "cancel",
}); });

View File

@@ -2,32 +2,19 @@ import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-checkbox";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import { import {
extractApiErrorMessage, extractApiErrorMessage,
ignoreSupervisorError, ignoreSupervisorError,
} from "../../../../src/data/hassio/common"; } from "../../../../src/data/hassio/common";
import {
SupervisorFrontendPrefrences,
fetchSupervisorFrontendPreferences,
saveSupervisorFrontendPreferences,
} from "../../../../src/data/supervisor/supervisor";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot"; import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update"; import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
import memoizeOne from "memoize-one";
const snapshot_before_update = memoizeOne(
(slug: string, frontendPrefrences: SupervisorFrontendPrefrences) =>
slug in frontendPrefrences.snapshot_before_update
? frontendPrefrences.snapshot_before_update[slug]
: true
);
@customElement("dialog-supervisor-update") @customElement("dialog-supervisor-update")
class DialogSupervisorUpdate extends LitElement { class DialogSupervisorUpdate extends LitElement {
@@ -35,12 +22,12 @@ class DialogSupervisorUpdate extends LitElement {
@state() private _opened = false; @state() private _opened = false;
@state() private _createSnapshot = true;
@state() private _action: "snapshot" | "update" | null = null; @state() private _action: "snapshot" | "update" | null = null;
@state() private _error?: string; @state() private _error?: string;
@state() private _frontendPrefrences?: SupervisorFrontendPrefrences;
@state() @state()
private _dialogParams?: SupervisorDialogSupervisorUpdateParams; private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
@@ -49,30 +36,27 @@ class DialogSupervisorUpdate extends LitElement {
): Promise<void> { ): Promise<void> {
this._opened = true; this._opened = true;
this._dialogParams = params; this._dialogParams = params;
this._frontendPrefrences = await fetchSupervisorFrontendPreferences(
this.hass
);
await this.updateComplete; await this.updateComplete;
} }
public closeDialog(): void { public closeDialog(): void {
this._action = null; this._action = null;
this._createSnapshot = true;
this._error = undefined; this._error = undefined;
this._dialogParams = undefined; this._dialogParams = undefined;
this._frontendPrefrences = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
public focus(): void { public focus(): void {
this.updateComplete.then(() => this.updateComplete.then(() =>
(this.shadowRoot?.querySelector( (
"[dialogInitialFocus]" this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
) as HTMLElement)?.focus() )?.focus()
); );
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._dialogParams || !this._frontendPrefrences) { if (!this._dialogParams) {
return html``; return html``;
} }
return html` return html`
@@ -98,16 +82,6 @@ class DialogSupervisorUpdate extends LitElement {
</div> </div>
<ha-settings-row> <ha-settings-row>
<ha-checkbox
.checked=${snapshot_before_update(
this._dialogParams.slug,
this._frontendPrefrences
)}
haptic
@click=${this._toggleSnapshot}
slot="prefix"
>
</ha-checkbox>
<span slot="heading"> <span slot="heading">
${this._dialogParams.supervisor.localize( ${this._dialogParams.supervisor.localize(
"dialog.update.snapshot" "dialog.update.snapshot"
@@ -120,6 +94,12 @@ class DialogSupervisorUpdate extends LitElement {
this._dialogParams.name this._dialogParams.name
)} )}
</span> </span>
<ha-switch
.checked=${this._createSnapshot}
haptic
@click=${this._toggleSnapshot}
>
</ha-switch>
</ha-settings-row> </ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction"> <mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this._dialogParams.supervisor.localize("common.cancel")} ${this._dialogParams.supervisor.localize("common.cancel")}
@@ -153,27 +133,12 @@ class DialogSupervisorUpdate extends LitElement {
`; `;
} }
private async _toggleSnapshot(): Promise<void> { private _toggleSnapshot() {
this._frontendPrefrences!.snapshot_before_update[ this._createSnapshot = !this._createSnapshot;
this._dialogParams!.slug
] = !snapshot_before_update(
this._dialogParams!.slug,
this._frontendPrefrences!
);
await saveSupervisorFrontendPreferences(
this.hass,
this._frontendPrefrences!
);
} }
private async _update() { private async _update() {
if ( if (this._createSnapshot) {
snapshot_before_update(
this._dialogParams!.slug,
this._frontendPrefrences!
)
) {
this._action = "snapshot"; this._action = "snapshot";
try { try {
await createHassioPartialSnapshot( await createHassioPartialSnapshot(
@@ -193,8 +158,8 @@ class DialogSupervisorUpdate extends LitElement {
} catch (err) { } catch (err) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) { if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err); this._error = extractApiErrorMessage(err);
this._action = null;
} }
this._action = null;
return; return;
} }

View File

@@ -5,7 +5,6 @@ export interface SupervisorDialogSupervisorUpdateParams {
supervisor: Supervisor; supervisor: Supervisor;
name: string; name: string;
version: string; version: string;
slug: string;
snapshotParams: any; snapshotParams: any;
updateHandler: () => Promise<void>; updateHandler: () => Promise<void>;
} }

View File

@@ -121,7 +121,7 @@ export class HassioMain extends SupervisorBaseElement {
} }
} else { } else {
themeName = themeName =
((this.hass.selectedTheme as unknown) as string) || (this.hass.selectedTheme as unknown as string) ||
this.hass.themes.default_theme; this.hass.themes.default_theme;
} }

View File

@@ -1,19 +1,19 @@
import { html, LitElement, TemplateResult } from "lit";
import { sanitizeUrl } from "@braintree/sanitize-url"; import { sanitizeUrl } from "@braintree/sanitize-url";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../src/common/navigate";
import { import {
createSearchParam, createSearchParam,
extractSearchParamsObject, extractSearchParamsObject,
} from "../../src/common/url/search-params"; } from "../../src/common/url/search-params";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import "../../src/layouts/hass-error-screen"; import "../../src/layouts/hass-error-screen";
import { import {
ParamType, ParamType,
Redirect, Redirect,
Redirects, Redirects,
} from "../../src/panels/my/ha-panel-my"; } from "../../src/panels/my/ha-panel-my";
import { navigate } from "../../src/common/navigate";
import { HomeAssistant, Route } from "../../src/types"; import { HomeAssistant, Route } from "../../src/types";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { customElement, property, state } from "lit/decorators";
const REDIRECTS: Redirects = { const REDIRECTS: Redirects = {
supervisor: { supervisor: {

View File

@@ -86,10 +86,8 @@ export class SupervisorBaseElement extends urlSyncMixin(
const unsubs = Object.keys(this._unsubs); const unsubs = Object.keys(this._unsubs);
for (const collection of Object.keys(this._collections)) { for (const collection of Object.keys(this._collections)) {
if (!unsubs.includes(collection)) { if (!unsubs.includes(collection)) {
this._unsubs[collection] = this._collections[ this._unsubs[collection] = this._collections[collection].subscribe(
collection (data) => this._updateSupervisor({ [collection]: data })
].subscribe((data) =>
this._updateSupervisor({ [collection]: data })
); );
} }
} }

View File

@@ -164,7 +164,6 @@ class HassioCoreInfo extends LitElement {
showDialogSupervisorUpdate(this, { showDialogSupervisorUpdate(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
name: "Home Assistant Core", name: "Home Assistant Core",
slug: "core",
version: this.supervisor.core.version_latest, version: this.supervisor.core.version_latest,
snapshotParams: { snapshotParams: {
name: `core_${this.supervisor.core.version}`, name: `core_${this.supervisor.core.version}`,

View File

@@ -113,7 +113,7 @@ class HassioHostInfo extends LitElement {
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
${!this.supervisor.host.features.includes("hassos") ${!this.supervisor.host.features.includes("haos")
? html`<ha-settings-row> ? html`<ha-settings-row>
<span slot="heading"> <span slot="heading">
${this.supervisor.localize("system.host.docker_version")} ${this.supervisor.localize("system.host.docker_version")}
@@ -190,7 +190,7 @@ class HassioHostInfo extends LitElement {
<mwc-list-item> <mwc-list-item>
${this.supervisor.localize("system.host.hardware")} ${this.supervisor.localize("system.host.hardware")}
</mwc-list-item> </mwc-list-item>
${this.supervisor.host.features.includes("hassos") ${this.supervisor.host.features.includes("haos")
? html`<mwc-list-item> ? html`<mwc-list-item>
${this.supervisor.localize("system.host.import_from_usb")} ${this.supervisor.localize("system.host.import_from_usb")}
</mwc-list-item>` </mwc-list-item>`

View File

@@ -1,4 +1,4 @@
module.exports = { module.exports = {
"*.{js,ts}": "eslint --fix", "*.{js,ts}": 'eslint --ignore-pattern "**/build-scripts/**/*.js" --fix',
"!(/translations)*.{js,ts,json,css,md,html}": "prettier --write", "!(/translations)*.{js,ts,json,css,md,html}": "prettier --write",
}; };

3
netlify.toml Normal file
View File

@@ -0,0 +1,3 @@
[build.environment]
YARN_VERSION = "1.22.11"
NODE_OPTIONS = "--max_old_space_size=4096"

View File

@@ -42,72 +42,72 @@
"@fullcalendar/daygrid": "5.1.0", "@fullcalendar/daygrid": "5.1.0",
"@fullcalendar/interaction": "5.1.0", "@fullcalendar/interaction": "5.1.0",
"@fullcalendar/list": "5.1.0", "@fullcalendar/list": "5.1.0",
"@lit-labs/virtualizer": "^0.6.0", "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch",
"@material/chips": "=12.0.0-canary.1a8d06483.0", "@material/chips": "12.0.0-canary.22d29cbb4.0",
"@material/mwc-button": "canary", "@material/data-table": "12.0.0-canary.22d29cbb4.0",
"@material/mwc-checkbox": "canary", "@material/mwc-button": "0.22.1",
"@material/mwc-circular-progress": "canary", "@material/mwc-checkbox": "0.22.1",
"@material/mwc-dialog": "canary", "@material/mwc-circular-progress": "0.22.1",
"@material/mwc-fab": "canary", "@material/mwc-dialog": "0.22.1",
"@material/mwc-formfield": "canary", "@material/mwc-fab": "0.22.1",
"@material/mwc-icon-button": "canary", "@material/mwc-formfield": "0.22.1",
"@material/mwc-list": "canary", "@material/mwc-icon-button": "0.22.1",
"@material/mwc-menu": "canary", "@material/mwc-linear-progress": "0.22.1",
"@material/mwc-radio": "canary", "@material/mwc-list": "0.22.1",
"@material/mwc-ripple": "canary", "@material/mwc-menu": "0.22.1",
"@material/mwc-switch": "canary", "@material/mwc-radio": "0.22.1",
"@material/mwc-tab": "canary", "@material/mwc-ripple": "0.22.1",
"@material/mwc-tab-bar": "canary", "@material/mwc-switch": "0.22.1",
"@material/top-app-bar": "=12.0.0-canary.1a8d06483.0", "@material/mwc-tab": "0.22.1",
"@material/mwc-tab-bar": "0.22.1",
"@material/top-app-bar": "12.0.0-canary.22d29cbb4.0",
"@mdi/js": "5.9.55", "@mdi/js": "5.9.55",
"@mdi/svg": "5.9.55", "@mdi/svg": "5.9.55",
"@polymer/app-layout": "^3.0.2", "@polymer/app-layout": "^3.1.0",
"@polymer/app-storage": "^3.0.2",
"@polymer/iron-autogrow-textarea": "^3.0.1",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
"@polymer/iron-input": "^3.0.1", "@polymer/iron-input": "^3.0.1",
"@polymer/iron-overlay-behavior": "^3.0.2", "@polymer/iron-overlay-behavior": "^3.0.3",
"@polymer/iron-resizable-behavior": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-checkbox": "^3.1.0", "@polymer/paper-checkbox": "^3.1.0",
"@polymer/paper-dialog": "^3.0.1", "@polymer/paper-dialog": "^3.0.1",
"@polymer/paper-dialog-behavior": "^3.0.1", "@polymer/paper-dialog-behavior": "^3.0.1",
"@polymer/paper-dialog-scrollable": "^3.0.1", "@polymer/paper-dialog-scrollable": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.0.1", "@polymer/paper-dropdown-menu": "^3.2.0",
"@polymer/paper-input": "^3.0.1", "@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1", "@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1", "@polymer/paper-listbox": "^3.0.1",
"@polymer/paper-menu-button": "^3.0.1", "@polymer/paper-menu-button": "^3.1.0",
"@polymer/paper-progress": "^3.0.1", "@polymer/paper-progress": "^3.0.1",
"@polymer/paper-radio-button": "^3.0.1", "@polymer/paper-radio-button": "^3.0.1",
"@polymer/paper-radio-group": "^3.0.1", "@polymer/paper-radio-group": "^3.0.1",
"@polymer/paper-ripple": "^3.0.1", "@polymer/paper-ripple": "^3.0.2",
"@polymer/paper-slider": "^3.0.1", "@polymer/paper-slider": "^3.0.1",
"@polymer/paper-styles": "^3.0.1", "@polymer/paper-styles": "^3.0.1",
"@polymer/paper-tabs": "^3.0.1", "@polymer/paper-tabs": "^3.1.0",
"@polymer/paper-toast": "^3.0.1", "@polymer/paper-toast": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0", "@polymer/polymer": "3.4.1",
"@thomasloven/round-slider": "0.5.2", "@thomasloven/round-slider": "0.5.2",
"@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-combo-box": "^20.0.1",
"@vaadin/vaadin-date-picker": "^4.0.7", "@vaadin/vaadin-date-picker": "^20.0.1",
"@vibrant/color": "^3.2.1-alpha.1", "@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1", "@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
"@vue/web-component-wrapper": "^1.2.0", "@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/webcomponentsjs": "^2.2.7", "@webcomponents/webcomponentsjs": "^2.2.10",
"chart.js": "^2.9.4", "chart.js": "^3.3.2",
"chartjs-chart-timeline": "^0.4.0",
"comlink": "^4.3.1", "comlink": "^4.3.1",
"core-js": "^3.6.5", "core-js": "^3.15.2",
"cropperjs": "^1.5.11", "cropperjs": "^1.5.11",
"date-fns": "^2.22.1",
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"fecha": "^4.2.0", "fecha": "^4.2.0",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^1.0.5", "hls.js": "^1.0.7",
"home-assistant-js-websocket": "^5.10.0", "home-assistant-js-websocket": "^5.11.1",
"idb-keyval": "^5.0.5", "idb-keyval": "^5.0.5",
"intl-messageformat": "^9.6.16", "intl-messageformat": "^9.6.16",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
@@ -122,7 +122,7 @@
"proxy-polyfill": "^0.3.1", "proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1", "punycode": "^2.1.1",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"regenerator-runtime": "^0.13.2", "regenerator-runtime": "^0.13.8",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2", "sortablejs": "^1.10.2",
@@ -144,17 +144,17 @@
"xss": "^1.0.9" "xss": "^1.0.9"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.3", "@babel/core": "^7.14.6",
"@babel/plugin-external-helpers": "^7.12.13", "@babel/plugin-external-helpers": "^7.14.5",
"@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-decorators": "^7.13.15", "@babel/plugin-proposal-decorators": "^7.14.5",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
"@babel/plugin-proposal-object-rest-spread": "^7.13.8", "@babel/plugin-proposal-object-rest-spread": "^7.14.7",
"@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-proposal-optional-chaining": "^7.14.5",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/preset-env": "^7.14.2", "@babel/preset-env": "^7.14.7",
"@babel/preset-typescript": "^7.13.0", "@babel/preset-typescript": "^7.14.5",
"@koa/cors": "^3.1.0", "@koa/cors": "^3.1.0",
"@open-wc/dev-server-hmr": "^0.0.2", "@open-wc/dev-server-hmr": "^0.0.2",
"@rollup/plugin-babel": "^5.2.1", "@rollup/plugin-babel": "^5.2.1",
@@ -171,22 +171,22 @@
"@types/mocha": "^8.2.2", "@types/mocha": "^8.2.2",
"@types/sortablejs": "^1.10.6", "@types/sortablejs": "^1.10.6",
"@types/webspeechapi": "^0.0.29", "@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/eslint-plugin": "^4.28.3",
"@typescript-eslint/parser": "^4.22.0", "@typescript-eslint/parser": "^4.28.3",
"@web/dev-server": "^0.0.24", "@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11", "@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^8.1.0", "babel-loader": "^8.2.2",
"chai": "^4.3.4", "chai": "^4.3.4",
"cpx": "^1.5.0",
"del": "^4.0.0", "del": "^4.0.0",
"eslint": "^7.25.0", "eslint": "^7.30.0",
"eslint-config-airbnb-typescript": "^12.3.1", "eslint-config-airbnb-typescript": "^12.3.1",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-webpack": "^0.13.0", "eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-disable": "^2.0.1", "eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.23.4",
"eslint-plugin-lit": "^1.3.0", "eslint-plugin-lit": "^1.5.1",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-unused-imports": "^1.1.2",
"eslint-plugin-wc": "^1.3.0", "eslint-plugin-wc": "^1.3.0",
"fancy-log": "^1.3.3", "fancy-log": "^1.3.3",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
@@ -198,7 +198,7 @@
"gulp-zopfli-green": "^3.0.1", "gulp-zopfli-green": "^3.0.1",
"html-minifier": "^4.0.0", "html-minifier": "^4.0.0",
"husky": "^1.3.1", "husky": "^1.3.1",
"lint-staged": "^10.5.4", "lint-staged": "^11.0.1",
"lit-analyzer": "^1.2.1", "lit-analyzer": "^1.2.1",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"magic-string": "^0.25.7", "magic-string": "^0.25.7",
@@ -207,8 +207,7 @@
"mocha": "^8.4.0", "mocha": "^8.4.0",
"object-hash": "^2.0.3", "object-hash": "^2.0.3",
"open": "^7.0.4", "open": "^7.0.4",
"prettier": "^2.0.4", "prettier": "^2.3.2",
"raw-loader": "^2.0.0",
"require-dir": "^1.2.0", "require-dir": "^1.2.0",
"rollup": "^2.8.2", "rollup": "^2.8.2",
"rollup-plugin-string": "^3.0.0", "rollup-plugin-string": "^3.0.0",
@@ -218,23 +217,22 @@
"sinon": "^11.0.0", "sinon": "^11.0.0",
"source-map-url": "^0.4.0", "source-map-url": "^0.4.0",
"systemjs": "^6.3.2", "systemjs": "^6.3.2",
"terser-webpack-plugin": "^5.1.2", "terser-webpack-plugin": "^5.1.4",
"ts-lit-plugin": "^1.2.1", "ts-lit-plugin": "^1.2.1",
"ts-mocha": "^8.0.0", "ts-mocha": "^8.0.0",
"typescript": "^4.2.4", "typescript": "^4.3.5",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",
"webpack": "^5.24.1", "webpack": "^5.43.0",
"webpack-cli": "^4.5.0", "webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2", "webpack-dev-server": "^3.11.2",
"webpack-manifest-plugin": "^3.0.0", "webpack-manifest-plugin": "^3.1.1",
"workbox-build": "^6.1.5" "workbox-build": "^6.1.5"
}, },
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page", "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569",
"resolutions": { "resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.2.10",
"@polymer/polymer": "3.1.0",
"lit-html": "2.0.0-rc.3", "lit-html": "2.0.0-rc.3",
"lit-element": "3.0.0-rc.2" "lit-element": "3.0.0-rc.2"
}, },

View File

@@ -9,12 +9,6 @@ if [ -z "${DEVCONTAINER}" ]; then
exit 1 exit 1
fi fi
if [ ! -z "${CODESPACES}" ]; then
WORKSPACE="/root/workspace/frontend"
else
WORKSPACE="/workspaces/frontend"
fi
if [ -z $(which hass) ]; then if [ -z $(which hass) ]; then
echo "Installing Home Asstant core from dev." echo "Installing Home Asstant core from dev."
python3 -m pip install --upgrade \ python3 -m pip install --upgrade \
@@ -22,9 +16,9 @@ if [ -z $(which hass) ]; then
git+git://github.com/home-assistant/home-assistant.git@dev git+git://github.com/home-assistant/home-assistant.git@dev
fi fi
if [ ! -d "${WORKSPACE}/config" ]; then if [ ! -d "/workspaces/frontend/config" ]; then
echo "Creating default configuration." echo "Creating default configuration."
mkdir -p "${WORKSPACE}/config"; mkdir -p "/workspaces/frontend/config";
hass --script ensure_config -c config hass --script ensure_config -c config
echo "demo: echo "demo:
@@ -32,24 +26,24 @@ logger:
default: info default: info
logs: logs:
homeassistant.components.frontend: debug homeassistant.components.frontend: debug
" >> "${WORKSPACE}/config/configuration.yaml" " >> /workspaces/frontend/config/configuration.yaml
if [ ! -z "${HASSIO}" ]; then if [ ! -z "${HASSIO}" ]; then
echo " echo "
# frontend: # frontend:
# development_repo: ${WORKSPACE} # development_repo: /workspaces/frontend
hassio: hassio:
development_repo: ${WORKSPACE}" >> "${WORKSPACE}/config/configuration.yaml" development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml
else else
echo " echo "
frontend: frontend:
development_repo: ${WORKSPACE} development_repo: /workspaces/frontend
# hassio: # hassio:
# development_repo: ${WORKSPACE}" >> "${WORKSPACE}/config/configuration.yaml" # development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml
fi fi
fi fi
hass -c "${WORKSPACE}/config" hass -c /workspaces/frontend/config

View File

@@ -2,9 +2,9 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20210603.0", version="20210804.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer", url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors", author="The Home Assistant Authors",
author_email="hello@home-assistant.io", author_email="hello@home-assistant.io",
license="Apache-2.0", license="Apache-2.0",

View File

@@ -7,6 +7,7 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import "./ha-password-manager-polyfill";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import "../components/ha-form/ha-form"; import "../components/ha-form/ha-form";
import "../components/ha-markdown"; import "../components/ha-markdown";
@@ -20,7 +21,7 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
type State = "loading" | "error" | "step"; type State = "loading" | "error" | "step";
class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@property() public authProvider?: AuthProvider; @property({ attribute: false }) public authProvider?: AuthProvider;
@property() public clientId?: string; @property() public clientId?: string;
@@ -37,7 +38,15 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@state() private _errorMessage?: string; @state() private _errorMessage?: string;
protected render() { protected render() {
return html` <form>${this._renderForm()}</form> `; return html`
<form>${this._renderForm()}</form>
<ha-password-manager-polyfill
.step=${this._step}
.stepData=${this._stepData}
@form-submitted=${this._handleSubmit}
@value-changed=${this._stepDataChanged}
></ha-password-manager-polyfill>
`;
} }
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
@@ -231,11 +240,17 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
await this.updateComplete; await this.updateComplete;
// 100ms to give all the form elements time to initialize. // 100ms to give all the form elements time to initialize.
setTimeout(() => { setTimeout(() => {
const form = this.shadowRoot!.querySelector("ha-form"); const form = this.renderRoot.querySelector("ha-form");
if (form) { if (form) {
(form as any).focus(); (form as any).focus();
} }
}, 100); }, 100);
setTimeout(() => {
this.renderRoot.querySelector(
"ha-password-manager-polyfill"
)!.boundingRect = this.getBoundingClientRect();
}, 500);
} }
private _stepDataChanged(ev: CustomEvent) { private _stepDataChanged(ev: CustomEvent) {
@@ -329,3 +344,9 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
} }
} }
customElements.define("ha-auth-flow", HaAuthFlow); customElements.define("ha-auth-flow", HaAuthFlow);
declare global {
interface HTMLElementTagNameMap {
"ha-auth-flow": HaAuthFlow;
}
}

View File

@@ -0,0 +1,110 @@
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { HaFormSchema } from "../components/ha-form/ha-form";
import { DataEntryFlowStep } from "../data/data_entry_flow";
declare global {
interface HTMLElementTagNameMap {
"ha-password-manager-polyfill": HaPasswordManagerPolyfill;
}
interface HASSDomEvents {
"form-submitted": undefined;
}
}
const ENABLED_HANDLERS = [
"homeassistant",
"legacy_api_password",
"command_line",
];
@customElement("ha-password-manager-polyfill")
export class HaPasswordManagerPolyfill extends LitElement {
@property({ attribute: false }) public step?: DataEntryFlowStep;
@property({ attribute: false }) public stepData: any;
@property({ attribute: false }) public boundingRect?: DOMRect;
protected createRenderRoot() {
// Add under document body so the element isn't placed inside any shadow roots
return document.body;
}
private get styles() {
return `
.password-manager-polyfill {
position: absolute;
top: ${this.boundingRect?.y || 148}px;
left: calc(50% - ${(this.boundingRect?.width || 360) / 2}px);
width: ${this.boundingRect?.width || 360}px;
opacity: 0;
z-index: -1;
}
.password-manager-polyfill input {
width: 100%;
height: 62px;
padding: 0;
border: 0;
}
.password-manager-polyfill input[type="submit"] {
width: 0;
height: 0;
}
`;
}
protected render(): TemplateResult {
if (
this.step &&
this.step.type === "form" &&
this.step.step_id === "init" &&
ENABLED_HANDLERS.includes(this.step.handler[0])
) {
return html`
<form
class="password-manager-polyfill"
aria-hidden="true"
@submit=${this._handleSubmit}
>
${this.step.data_schema.map((input) => this.render_input(input))}
<input type="submit" />
<style>
${this.styles}
</style>
</form>
`;
}
return html``;
}
private render_input(schema: HaFormSchema): TemplateResult | string {
const inputType = schema.name.includes("password") ? "password" : "text";
if (schema.type !== "string") {
return "";
}
return html`
<input
tabindex="-1"
.id=${schema.name}
.type=${inputType}
.value=${this.stepData[schema.name] || ""}
@input=${this._valueChanged}
/>
`;
}
private _handleSubmit(ev: Event) {
ev.preventDefault();
fireEvent(this, "form-submitted");
}
private _valueChanged(ev: Event) {
const target = ev.target! as HTMLInputElement;
this.stepData = { ...this.stepData, [target.id]: target.value };
fireEvent(this, "value-changed", {
value: this.stepData,
});
}
}

View File

@@ -0,0 +1,63 @@
export const COLORS = [
"#377eb8",
"#984ea3",
"#00d2d5",
"#ff7f00",
"#af8d00",
"#7f80cd",
"#b3e900",
"#c42e60",
"#a65628",
"#f781bf",
"#8dd3c7",
"#bebada",
"#fb8072",
"#80b1d3",
"#fdb462",
"#fccde5",
"#bc80bd",
"#ffed6f",
"#c4eaff",
"#cf8c00",
"#1b9e77",
"#d95f02",
"#e7298a",
"#e6ab02",
"#a6761d",
"#0097ff",
"#00d067",
"#f43600",
"#4ba93b",
"#5779bb",
"#927acc",
"#97ee3f",
"#bf3947",
"#9f5b00",
"#f48758",
"#8caed6",
"#f2b94f",
"#eff26e",
"#e43872",
"#d9b100",
"#9d7a00",
"#698cff",
"#d9d9d9",
"#00d27e",
"#d06800",
"#009f82",
"#c49200",
"#cbe8ff",
"#fecddf",
"#c27eb6",
"#8cd2ce",
"#c4b8d9",
"#f883b0",
"#a49100",
"#f48800",
"#27d0df",
"#a04a9b",
];
export function getColorByIndex(index: number) {
return COLORS[index % COLORS.length];
}

View File

@@ -1,4 +1,4 @@
const luminosity = (rgb: [number, number, number]): number => { export const luminosity = (rgb: [number, number, number]): number => {
// http://www.w3.org/TR/WCAG20/#relativeluminancedef // http://www.w3.org/TR/WCAG20/#relativeluminancedef
const lum: [number, number, number] = [0, 0, 0]; const lum: [number, number, number] = [0, 0, 0];
for (let i = 0; i < rgb.length; i++) { for (let i = 0; i < rgb.length; i++) {

View File

@@ -42,6 +42,7 @@ export const FIXED_DOMAIN_ICONS = {
remote: "hass:remote", remote: "hass:remote",
scene: "hass:palette", scene: "hass:palette",
script: "hass:script-text", script: "hass:script-text",
select: "hass:format-list-bulleted",
sensor: "hass:eye", sensor: "hass:eye",
simple_alarm: "hass:bell", simple_alarm: "hass:bell",
sun: "hass:white-balance-sunny", sun: "hass:white-balance-sunny",
@@ -58,10 +59,11 @@ export const FIXED_DEVICE_CLASS_ICONS = {
current: "hass:current-ac", current: "hass:current-ac",
carbon_dioxide: "mdi:molecule-co2", carbon_dioxide: "mdi:molecule-co2",
carbon_monoxide: "mdi:molecule-co", carbon_monoxide: "mdi:molecule-co",
energy: "hass:flash", energy: "hass:lightning-bolt",
humidity: "hass:water-percent", humidity: "hass:water-percent",
illuminance: "hass:brightness-5", illuminance: "hass:brightness-5",
temperature: "hass:thermometer", temperature: "hass:thermometer",
monetary: "mdi:cash",
pressure: "hass:gauge", pressure: "hass:gauge",
power: "hass:flash", power: "hass:flash",
power_factor: "hass:angle-acute", power_factor: "hass:angle-acute",
@@ -83,6 +85,7 @@ export const DOMAINS_WITH_CARD = [
"number", "number",
"scene", "scene",
"script", "script",
"select",
"timer", "timer",
"vacuum", "vacuum",
"water_heater", "water_heater",
@@ -121,6 +124,7 @@ export const DOMAINS_HIDE_MORE_INFO = [
"input_text", "input_text",
"number", "number",
"scene", "scene",
"select",
]; ];
/** Domains that should have the history hidden in the more info dialog. */ /** Domains that should have the history hidden in the more info dialog. */

View File

@@ -26,6 +26,9 @@ function checkToLocaleStringSupportsOptions() {
return false; return false;
} }
export const toLocaleDateStringSupportsOptions = checkToLocaleDateStringSupportsOptions(); export const toLocaleDateStringSupportsOptions =
export const toLocaleTimeStringSupportsOptions = checkToLocaleTimeStringSupportsOptions(); checkToLocaleDateStringSupportsOptions();
export const toLocaleStringSupportsOptions = checkToLocaleStringSupportsOptions(); export const toLocaleTimeStringSupportsOptions =
checkToLocaleTimeStringSupportsOptions();
export const toLocaleStringSupportsOptions =
checkToLocaleStringSupportsOptions();

View File

@@ -17,6 +17,19 @@ export const formatDate = toLocaleDateStringSupportsOptions
formatDateMem(locale).format(dateObj) formatDateMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "longDate"); : (dateObj: Date) => format(dateObj, "longDate");
const formatDateShortMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
day: "numeric",
month: "short",
})
);
export const formatDateShort = toLocaleDateStringSupportsOptions
? (dateObj: Date, locale: FrontendLocaleData) =>
formatDateShortMem(locale).format(dateObj)
: (dateObj: Date) => format(dateObj, "shortDate");
const formatDateWeekdayMem = memoizeOne( const formatDateWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) => (locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, { new Intl.DateTimeFormat(locale.language, {

View File

@@ -82,67 +82,71 @@ class Storage {
const storage = new Storage(); const storage = new Storage();
export const LocalStorage = ( export const LocalStorage =
storageKey?: string, (
property?: boolean, storageKey?: string,
propertyOptions?: PropertyDeclaration property?: boolean,
): any => (clsElement: ClassElement) => { propertyOptions?: PropertyDeclaration
const key = String(clsElement.key); ): any =>
storageKey = storageKey || String(clsElement.key); (clsElement: ClassElement) => {
const initVal = clsElement.initializer ? clsElement.initializer() : undefined; const key = String(clsElement.key);
storageKey = storageKey || String(clsElement.key);
const initVal = clsElement.initializer
? clsElement.initializer()
: undefined;
storage.addFromStorage(storageKey); storage.addFromStorage(storageKey);
const subscribe = (el: ReactiveElement): UnsubscribeFunc => const subscribe = (el: ReactiveElement): UnsubscribeFunc =>
storage.subscribeChanges(storageKey!, (oldValue) => { storage.subscribeChanges(storageKey!, (oldValue) => {
el.requestUpdate(clsElement.key, oldValue); el.requestUpdate(clsElement.key, oldValue);
}); });
const getValue = (): any => const getValue = (): any =>
storage.hasKey(storageKey!) ? storage.getValue(storageKey!) : initVal; storage.hasKey(storageKey!) ? storage.getValue(storageKey!) : initVal;
const setValue = (el: ReactiveElement, value: any) => { const setValue = (el: ReactiveElement, value: any) => {
let oldValue: unknown | undefined; let oldValue: unknown | undefined;
if (property) {
oldValue = getValue();
}
storage.setValue(storageKey!, value);
if (property) {
el.requestUpdate(clsElement.key, oldValue);
}
};
return {
kind: "method",
placement: "prototype",
key: clsElement.key,
descriptor: {
set(this: ReactiveElement, value: unknown) {
setValue(this, value);
},
get() {
return getValue();
},
enumerable: true,
configurable: true,
},
finisher(cls: typeof ReactiveElement) {
if (property) { if (property) {
const connectedCallback = cls.prototype.connectedCallback; oldValue = getValue();
const disconnectedCallback = cls.prototype.disconnectedCallback;
cls.prototype.connectedCallback = function () {
connectedCallback.call(this);
this[`__unbsubLocalStorage${key}`] = subscribe(this);
};
cls.prototype.disconnectedCallback = function () {
disconnectedCallback.call(this);
this[`__unbsubLocalStorage${key}`]();
};
cls.createProperty(clsElement.key, {
noAccessor: true,
...propertyOptions,
});
} }
}, storage.setValue(storageKey!, value);
if (property) {
el.requestUpdate(clsElement.key, oldValue);
}
};
return {
kind: "method",
placement: "prototype",
key: clsElement.key,
descriptor: {
set(this: ReactiveElement, value: unknown) {
setValue(this, value);
},
get() {
return getValue();
},
enumerable: true,
configurable: true,
},
finisher(cls: typeof ReactiveElement) {
if (property) {
const connectedCallback = cls.prototype.connectedCallback;
const disconnectedCallback = cls.prototype.disconnectedCallback;
cls.prototype.connectedCallback = function () {
connectedCallback.call(this);
this[`__unbsubLocalStorage${key}`] = subscribe(this);
};
cls.prototype.disconnectedCallback = function () {
disconnectedCallback.call(this);
this[`__unbsubLocalStorage${key}`]();
};
cls.createProperty(clsElement.key, {
noAccessor: true,
...propertyOptions,
});
}
},
};
}; };
};

View File

@@ -1,33 +1,33 @@
import type { LitElement } from "lit"; import type { LitElement } from "lit";
import type { ClassElement } from "../../types"; import type { ClassElement } from "../../types";
export const restoreScroll = (selector: string): any => ( export const restoreScroll =
element: ClassElement (selector: string): any =>
) => ({ (element: ClassElement) => ({
kind: "method", kind: "method",
placement: "prototype", placement: "prototype",
key: element.key, key: element.key,
descriptor: { descriptor: {
set(this: LitElement, value: number) { set(this: LitElement, value: number) {
this[`__${String(element.key)}`] = value; this[`__${String(element.key)}`] = value;
},
get(this: LitElement) {
return this[`__${String(element.key)}`];
},
enumerable: true,
configurable: true,
}, },
get(this: LitElement) { finisher(cls: typeof LitElement) {
return this[`__${String(element.key)}`]; const connectedCallback = cls.prototype.connectedCallback;
}, cls.prototype.connectedCallback = function () {
enumerable: true, connectedCallback.call(this);
configurable: true, if (this[element.key]) {
}, const target = this.renderRoot.querySelector(selector);
finisher(cls: typeof LitElement) { if (!target) {
const connectedCallback = cls.prototype.connectedCallback; return;
cls.prototype.connectedCallback = function () { }
connectedCallback.call(this); target.scrollTop = this[element.key];
if (this[element.key]) {
const target = this.renderRoot.querySelector(selector);
if (!target) {
return;
} }
target.scrollTop = this[element.key]; };
} },
}; });
},
});

View File

@@ -21,6 +21,16 @@ export const computeStateDisplay = (
} }
if (stateObj.attributes.unit_of_measurement) { if (stateObj.attributes.unit_of_measurement) {
if (stateObj.attributes.device_class === "monetary") {
try {
return formatNumber(compareState, locale, {
style: "currency",
currency: stateObj.attributes.unit_of_measurement,
});
} catch (_err) {
// fallback to default
}
}
return `${formatNumber(compareState, locale)} ${ return `${formatNumber(compareState, locale)} ${
stateObj.attributes.unit_of_measurement stateObj.attributes.unit_of_measurement
}`; }`;
@@ -29,37 +39,61 @@ export const computeStateDisplay = (
const domain = computeStateDomain(stateObj); const domain = computeStateDomain(stateObj);
if (domain === "input_datetime") { if (domain === "input_datetime") {
let date: Date; if (state) {
if (!stateObj.attributes.has_time) { // If trying to display an explicit state, need to parse the explict state to `Date` then format.
// Attributes aren't available, we have to use `state`.
try {
const components = state.split(" ");
if (components.length === 2) {
// Date and time.
return formatDateTime(new Date(components.join("T")), locale);
}
if (components.length === 1) {
if (state.includes("-")) {
// Date only.
return formatDate(new Date(`${state}T00:00`), locale);
}
if (state.includes(":")) {
// Time only.
const now = new Date();
return formatTime(
new Date(`${now.toISOString().split("T")[0]}T${state}`),
locale
);
}
}
return state;
} catch {
// Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case.
return state;
}
} else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date;
if (!stateObj.attributes.has_time) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day
);
return formatDate(date, locale);
}
if (!stateObj.attributes.has_date) {
date = new Date();
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
return formatTime(date, locale);
}
date = new Date( date = new Date(
stateObj.attributes.year, stateObj.attributes.year,
stateObj.attributes.month - 1, stateObj.attributes.month - 1,
stateObj.attributes.day stateObj.attributes.day,
);
return formatDate(date, locale);
}
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.hour,
stateObj.attributes.minute stateObj.attributes.minute
); );
return formatTime(date, locale); return formatDateTime(date, locale);
} }
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatDateTime(date, locale);
} }
if (domain === "humidifier") { if (domain === "humidifier") {

View File

@@ -43,7 +43,17 @@ export const domainIcon = (
: "hass:air-humidifier"; : "hass:air-humidifier";
case "lock": case "lock":
return compareState === "unlocked" ? "hass:lock-open" : "hass:lock"; switch (compareState) {
case "unlocked":
return "hass:lock-open";
case "jammed":
return "hass:lock-alert";
case "locking":
case "unlocking":
return "hass:lock-clock";
default:
return "hass:lock";
}
case "media_player": case "media_player":
return compareState === "playing" ? "hass:cast-connected" : "hass:cast"; return compareState === "playing" ? "hass:cast-connected" : "hass:cast";

View File

@@ -4,7 +4,7 @@ import { DEFAULT_DOMAIN_ICON } from "../const";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
import { domainIcon } from "./domain_icon"; import { domainIcon } from "./domain_icon";
export const stateIcon = (state: HassEntity) => { export const stateIcon = (state?: HassEntity) => {
if (!state) { if (!state) {
return DEFAULT_DOMAIN_ICON; return DEFAULT_DOMAIN_ICON;
} }

View File

@@ -0,0 +1,2 @@
export const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);

View File

@@ -0,0 +1,2 @@
export const round = (value: number, precision = 2): number =>
Math.round(value * 10 ** precision) / 10 ** precision;

View File

@@ -67,9 +67,11 @@ class SearchInput extends LitElement {
changedProps.has("noUnderline") && changedProps.has("noUnderline") &&
(this.noUnderline || changedProps.get("noUnderline") !== undefined) (this.noUnderline || changedProps.get("noUnderline") !== undefined)
) { ) {
(this._input.inputElement!.parentElement!.shadowRoot!.querySelector( (
"div.unfocused-line" this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
) as HTMLElement).style.display = this.noUnderline ? "none" : "block"; "div.unfocused-line"
) as HTMLElement
).style.display = this.noUnderline ? "none" : "block";
} }
} }

View File

@@ -1,4 +1,22 @@
import { FrontendLocaleData, NumberFormat } from "../../data/translation"; import { FrontendLocaleData, NumberFormat } from "../../data/translation";
import { round } from "../number/round";
export const numberFormatToLocale = (
localeOptions: FrontendLocaleData
): string | string[] | undefined => {
switch (localeOptions.number_format) {
case NumberFormat.comma_decimal:
return ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
case NumberFormat.decimal_comma:
return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
case NumberFormat.space_comma:
return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
case NumberFormat.system:
return undefined;
default:
return localeOptions.language;
}
};
/** /**
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility. * Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
@@ -9,27 +27,12 @@ import { FrontendLocaleData, NumberFormat } from "../../data/translation";
*/ */
export const formatNumber = ( export const formatNumber = (
num: string | number, num: string | number,
locale?: FrontendLocaleData, localeOptions?: FrontendLocaleData,
options?: Intl.NumberFormatOptions options?: Intl.NumberFormatOptions
): string => { ): string => {
let format: string | string[] | undefined; const locale = localeOptions
? numberFormatToLocale(localeOptions)
switch (locale?.number_format) { : undefined;
case NumberFormat.comma_decimal:
format = ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
break;
case NumberFormat.decimal_comma:
format = ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
break;
case NumberFormat.space_comma:
format = ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
break;
case NumberFormat.system:
format = undefined;
break;
default:
format = locale?.language;
}
// Polyfill for Number.isNaN, which is more reliable than the global isNaN() // Polyfill for Number.isNaN, which is more reliable than the global isNaN()
Number.isNaN = Number.isNaN =
@@ -39,13 +42,13 @@ export const formatNumber = (
}; };
if ( if (
localeOptions?.number_format !== NumberFormat.none &&
!Number.isNaN(Number(num)) && !Number.isNaN(Number(num)) &&
Intl && Intl
locale?.number_format !== NumberFormat.none
) { ) {
try { try {
return new Intl.NumberFormat( return new Intl.NumberFormat(
format, locale,
getDefaultFormatOptions(num, options) getDefaultFormatOptions(num, options)
).format(Number(num)); ).format(Number(num));
} catch (error) { } catch (error) {
@@ -58,7 +61,12 @@ export const formatNumber = (
).format(Number(num)); ).format(Number(num));
} }
} }
return num.toString(); if (typeof num === "string") {
return num;
}
return `${round(num, options?.maximumFractionDigits).toString()}${
options?.style === "currency" ? ` ${options.currency}` : ""
}`;
}; };
/** /**
@@ -70,7 +78,10 @@ const getDefaultFormatOptions = (
num: string | number, num: string | number,
options?: Intl.NumberFormatOptions options?: Intl.NumberFormatOptions
): Intl.NumberFormatOptions => { ): Intl.NumberFormatOptions => {
const defaultOptions: Intl.NumberFormatOptions = options || {}; const defaultOptions: Intl.NumberFormatOptions = {
maximumFractionDigits: 2,
...options,
};
if (typeof num !== "string") { if (typeof num !== "string") {
return defaultOptions; return defaultOptions;

View File

@@ -6,6 +6,7 @@
// 3. Disallow dates based on week number. // 3. Disallow dates based on week number.
// 4. Disallow dates only consisting of a year. // 4. Disallow dates only consisting of a year.
// https://regex101.com/r/kc5C14/3 // https://regex101.com/r/kc5C14/3
const regexp = /^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])[T| ](((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)(\8[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)$/; const regexp =
/^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])[T| ](((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)(\8[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)$/;
export const isTimestamp = (input: string): boolean => regexp.test(input); export const isTimestamp = (input: string): boolean => regexp.test(input);

View File

@@ -29,31 +29,28 @@ export const iconColorCSS = css`
} }
ha-icon[data-domain="climate"][data-state="cooling"] { ha-icon[data-domain="climate"][data-state="cooling"] {
color: var(--cool-color, #2b9af9); color: var(--cool-color, var(--state-climate-cool-color));
} }
ha-icon[data-domain="climate"][data-state="heating"] { ha-icon[data-domain="climate"][data-state="heating"] {
color: var(--heat-color, #ff8100); color: var(--heat-color, var(--state-climate-heat-color));
} }
ha-icon[data-domain="climate"][data-state="drying"] { ha-icon[data-domain="climate"][data-state="drying"] {
color: var(--dry-color, #efbd07); color: var(--dry-color, var(--state-climate-dry-color));
} }
ha-icon[data-domain="alarm_control_panel"] { ha-icon[data-domain="alarm_control_panel"] {
color: var(--alarm-color-armed, var(--label-badge-red)); color: var(--alarm-color-armed, var(--label-badge-red));
} }
ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] { ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
color: var(--alarm-color-disarmed, var(--label-badge-green)); color: var(--alarm-color-disarmed, var(--label-badge-green));
} }
ha-icon[data-domain="alarm_control_panel"][data-state="pending"], ha-icon[data-domain="alarm_control_panel"][data-state="pending"],
ha-icon[data-domain="alarm_control_panel"][data-state="arming"] { ha-icon[data-domain="alarm_control_panel"][data-state="arming"] {
color: var(--alarm-color-pending, var(--label-badge-yellow)); color: var(--alarm-color-pending, var(--label-badge-yellow));
animation: pulse 1s infinite; animation: pulse 1s infinite;
} }
ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] { ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
color: var(--alarm-color-triggered, var(--label-badge-red)); color: var(--alarm-color-triggered, var(--label-badge-red));
animation: pulse 1s infinite; animation: pulse 1s infinite;
@@ -73,11 +70,11 @@ export const iconColorCSS = css`
ha-icon[data-domain="plant"][data-state="problem"], ha-icon[data-domain="plant"][data-state="problem"],
ha-icon[data-domain="zwave"][data-state="dead"] { ha-icon[data-domain="zwave"][data-state="dead"] {
color: var(--error-state-color, #db4437); color: var(--state-icon-error-color);
} }
/* Color the icon if unavailable */ /* Color the icon if unavailable */
ha-icon[data-state="unavailable"] { ha-icon[data-state="unavailable"] {
color: var(--state-icon-unavailable-color); color: var(--state-unavailable-color);
} }
`; `;

View File

@@ -5,32 +5,20 @@
// as much as it can, without ever going more than once per `wait` duration; // as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass // but if you'd like to disable the execution on the leading edge, pass
// `false for leading`. To disable execution on the trailing edge, ditto. // `false for leading`. To disable execution on the trailing edge, ditto.
export const throttle = <T extends (...args) => unknown>( export const throttle = <T extends any[]>(
func: T, func: (...args: T) => void,
wait: number, wait: number,
leading = true, leading = true,
trailing = true trailing = true
): T => { ) => {
let timeout: number | undefined; let timeout: number | undefined;
let previous = 0; let previous = 0;
let context: any; return (...args: T): void => {
let args: any; const later = () => {
const later = () => { previous = leading === false ? 0 : Date.now();
previous = leading === false ? 0 : Date.now(); timeout = undefined;
timeout = undefined; func(...args);
func.apply(context, args); };
if (!timeout) {
context = null;
args = null;
}
};
// @ts-ignore
return function (...argmnts) {
// @ts-ignore
// @typescript-eslint/no-this-alias
context = this;
args = argmnts;
const now = Date.now(); const now = Date.now();
if (!previous && leading === false) { if (!previous && leading === false) {
previous = now; previous = now;
@@ -42,7 +30,7 @@ export const throttle = <T extends (...args) => unknown>(
timeout = undefined; timeout = undefined;
} }
previous = now; previous = now;
func.apply(context, args); func(...args);
} else if (!timeout && trailing !== false) { } else if (!timeout && trailing !== false) {
timeout = window.setTimeout(later, remaining); timeout = window.setTimeout(later, remaining);
} }

View File

@@ -64,18 +64,18 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
this.hass this.hass
.callService(this.domain, this.service, this.serviceData) .callService(this.domain, this.service, this.serviceData)
.then( .then(
function () { () => {
el.progress = false; el.progress = false;
el.$.progress.actionSuccess(); el.$.progress.actionSuccess();
eventData.success = true; eventData.success = true;
}, },
function () { () => {
el.progress = false; el.progress = false;
el.$.progress.actionError(); el.$.progress.actionError();
eventData.success = false; eventData.success = false;
} }
) )
.then(function () { .then(() => {
el.fire("hass-service-called", eventData); el.fire("hass-service-called", eventData);
}); });
} }

View File

@@ -5,7 +5,7 @@ import { customElement, property, query } from "lit/decorators";
import "../ha-circular-progress"; import "../ha-circular-progress";
@customElement("ha-progress-button") @customElement("ha-progress-button")
class HaProgressButton extends LitElement { export class HaProgressButton extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public progress = false; @property({ type: Boolean }) public progress = false;

View File

@@ -0,0 +1,197 @@
import { _adapters } from "chart.js";
import {
startOfSecond,
startOfMinute,
startOfHour,
startOfDay,
startOfWeek,
startOfMonth,
startOfQuarter,
startOfYear,
addMilliseconds,
addSeconds,
addMinutes,
addHours,
addDays,
addWeeks,
addMonths,
addQuarters,
addYears,
differenceInMilliseconds,
differenceInSeconds,
differenceInMinutes,
differenceInHours,
differenceInDays,
differenceInWeeks,
differenceInMonths,
differenceInQuarters,
differenceInYears,
endOfSecond,
endOfMinute,
endOfHour,
endOfDay,
endOfWeek,
endOfMonth,
endOfQuarter,
endOfYear,
} from "date-fns";
import { formatDate, formatDateShort } from "../../common/datetime/format_date";
import {
formatDateTime,
formatDateTimeWithSeconds,
} from "../../common/datetime/format_date_time";
import {
formatTime,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
const FORMATS = {
datetime: "datetime",
datetimeseconds: "datetimeseconds",
millisecond: "millisecond",
second: "second",
minute: "minute",
hour: "hour",
day: "day",
week: "week",
month: "month",
quarter: "quarter",
year: "year",
};
_adapters._date.override({
formats: () => FORMATS,
parse: (value: Date | number) => {
if (!(value instanceof Date)) {
return value;
}
return value.getTime();
},
format: function (time, fmt: keyof typeof FORMATS) {
switch (fmt) {
case "datetime":
return formatDateTime(new Date(time), this.options.locale);
case "datetimeseconds":
return formatDateTimeWithSeconds(new Date(time), this.options.locale);
case "millisecond":
return formatTimeWithSeconds(new Date(time), this.options.locale);
case "second":
return formatTimeWithSeconds(new Date(time), this.options.locale);
case "minute":
return formatTime(new Date(time), this.options.locale);
case "hour":
return formatTime(new Date(time), this.options.locale);
case "day":
return formatDateShort(new Date(time), this.options.locale);
case "week":
return formatDate(new Date(time), this.options.locale);
case "month":
return formatDate(new Date(time), this.options.locale);
case "quarter":
return formatDate(new Date(time), this.options.locale);
case "year":
return formatDate(new Date(time), this.options.locale);
default:
return "";
}
},
// @ts-ignore
add: (time, amount, unit) => {
switch (unit) {
case "millisecond":
return addMilliseconds(time, amount);
case "second":
return addSeconds(time, amount);
case "minute":
return addMinutes(time, amount);
case "hour":
return addHours(time, amount);
case "day":
return addDays(time, amount);
case "week":
return addWeeks(time, amount);
case "month":
return addMonths(time, amount);
case "quarter":
return addQuarters(time, amount);
case "year":
return addYears(time, amount);
default:
return time;
}
},
diff: (max, min, unit) => {
switch (unit) {
case "millisecond":
return differenceInMilliseconds(max, min);
case "second":
return differenceInSeconds(max, min);
case "minute":
return differenceInMinutes(max, min);
case "hour":
return differenceInHours(max, min);
case "day":
return differenceInDays(max, min);
case "week":
return differenceInWeeks(max, min);
case "month":
return differenceInMonths(max, min);
case "quarter":
return differenceInQuarters(max, min);
case "year":
return differenceInYears(max, min);
default:
return 0;
}
},
// @ts-ignore
startOf: (time, unit, weekday) => {
switch (unit) {
case "second":
return startOfSecond(time);
case "minute":
return startOfMinute(time);
case "hour":
return startOfHour(time);
case "day":
return startOfDay(time);
case "week":
return startOfWeek(time);
case "isoWeek":
return startOfWeek(time, {
weekStartsOn: +weekday! as 0 | 1 | 2 | 3 | 4 | 5 | 6,
});
case "month":
return startOfMonth(time);
case "quarter":
return startOfQuarter(time);
case "year":
return startOfYear(time);
default:
return time;
}
},
// @ts-ignore
endOf: (time, unit) => {
switch (unit) {
case "second":
return endOfSecond(time);
case "minute":
return endOfMinute(time);
case "hour":
return endOfHour(time);
case "day":
return endOfDay(time);
case "week":
return endOfWeek(time);
case "month":
return endOfMonth(time);
case "quarter":
return endOfQuarter(time);
case "year":
return endOfYear(time);
default:
return time;
}
},
});

View File

@@ -0,0 +1,349 @@
import type {
Chart,
ChartType,
ChartData,
ChartOptions,
TooltipModel,
} from "chart.js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { clamp } from "../../common/number/clamp";
interface Tooltip extends TooltipModel<any> {
top: string;
left: string;
}
@customElement("ha-chart-base")
export default class HaChartBase extends LitElement {
public chart?: Chart;
@property({ attribute: "chart-type", reflect: true })
public chartType: ChartType = "line";
@property({ attribute: false }) public data: ChartData = { datasets: [] };
@property({ attribute: false }) public options?: ChartOptions;
@property({ attribute: false }) public plugins?: any[];
@property({ type: Number }) public height?: number;
@state() private _chartHeight?: number;
@state() private _tooltip?: Tooltip;
@state() private _hiddenDatasets: Set<number> = new Set();
protected firstUpdated() {
this._setupChart();
this.data.datasets.forEach((dataset, index) => {
if (dataset.hidden) {
this._hiddenDatasets.add(index);
}
});
}
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this.hasUpdated || !this.chart) {
return;
}
if (changedProps.has("plugins")) {
this.chart.destroy();
this._setupChart();
return;
}
if (changedProps.has("chartType")) {
this.chart.config.type = this.chartType;
}
if (changedProps.has("data")) {
this.chart.data = this.data;
}
if (changedProps.has("options")) {
this.chart.options = this._createOptions();
}
this.chart.update("none");
}
protected render() {
return html`
${this.options?.plugins?.legend?.display === true
? html`<div class="chartLegend">
<ul>
${this.data.datasets.map(
(dataset, index) => html`<li
.datasetIndex=${index}
@click=${this._legendClick}
class=${classMap({
hidden: this._hiddenDatasets.has(index),
})}
>
<div
class="bullet"
style=${styleMap({
backgroundColor: dataset.backgroundColor as string,
borderColor: dataset.borderColor as string,
})}
></div>
${dataset.label}
</li>`
)}
</ul>
</div>`
: ""}
<div
class="chartContainer"
style=${styleMap({
height: `${this.height ?? this._chartHeight}px`,
overflow: this._chartHeight ? "initial" : "hidden",
})}
>
<canvas></canvas>
${this._tooltip
? html`<div
class="chartTooltip ${classMap({ [this._tooltip.yAlign]: true })}"
style=${styleMap({
top: this._tooltip.top,
left: this._tooltip.left,
})}
>
<div class="title">${this._tooltip.title}</div>
${this._tooltip.beforeBody
? html`<div class="beforeBody">
${this._tooltip.beforeBody}
</div>`
: ""}
<div>
<ul>
${this._tooltip.body.map(
(item, i) => html`<li>
<div
class="bullet"
style=${styleMap({
backgroundColor: this._tooltip!.labelColors[i]
.backgroundColor as string,
borderColor: this._tooltip!.labelColors[i]
.borderColor as string,
})}
></div>
${item.lines.join("\n")}
</li>`
)}
</ul>
</div>
${this._tooltip.footer.length
? html`<div class="footer">
${this._tooltip.footer.map((item) => html`${item}<br />`)}
</div>`
: ""}
</div>`
: ""}
</div>
`;
}
private async _setupChart() {
const ctx: CanvasRenderingContext2D = this.renderRoot
.querySelector("canvas")!
.getContext("2d")!;
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
const computedStyles = getComputedStyle(this);
ChartConstructor.defaults.borderColor =
computedStyles.getPropertyValue("--divider-color");
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
"--secondary-text-color"
);
this.chart = new ChartConstructor(ctx, {
type: this.chartType,
data: this.data,
options: this._createOptions(),
plugins: this._createPlugins(),
});
}
private _createOptions() {
return {
...this.options,
plugins: {
...this.options?.plugins,
tooltip: {
...this.options?.plugins?.tooltip,
enabled: false,
external: (context) => this._handleTooltip(context),
},
legend: {
...this.options?.plugins?.legend,
display: false,
},
},
};
}
private _createPlugins() {
return [
...(this.plugins || []),
{
id: "afterRenderHook",
afterRender: (chart) => {
this._chartHeight = chart.height;
},
legend: {
...this.options?.plugins?.legend,
display: false,
},
},
];
}
private _legendClick(ev) {
if (!this.chart) {
return;
}
const index = ev.currentTarget.datasetIndex;
if (this.chart.isDatasetVisible(index)) {
this.chart.setDatasetVisibility(index, false);
this._hiddenDatasets.add(index);
} else {
this.chart.setDatasetVisibility(index, true);
this._hiddenDatasets.delete(index);
}
this.chart.update("none");
this.requestUpdate("_hiddenDatasets");
}
private _handleTooltip(context: {
chart: Chart;
tooltip: TooltipModel<any>;
}) {
if (context.tooltip.opacity === 0) {
this._tooltip = undefined;
return;
}
this._tooltip = {
...context.tooltip,
top: this.chart!.canvas.offsetTop + context.tooltip.caretY + 12 + "px",
left:
this.chart!.canvas.offsetLeft +
clamp(context.tooltip.caretX, 100, this.clientWidth - 100) -
100 +
"px",
};
}
public updateChart = (): void => {
if (this.chart) {
this.chart.update();
}
};
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
.chartContainer {
overflow: hidden;
height: 0;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
canvas {
max-height: var(--chart-max-height, 400px);
}
.chartLegend {
text-align: center;
}
.chartLegend li {
cursor: pointer;
display: inline-flex;
padding: 0 8px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
align-items: center;
color: var(--secondary-text-color);
}
.chartLegend .hidden {
text-decoration: line-through;
}
.chartLegend .bullet,
.chartTooltip .bullet {
border-width: 1px;
border-style: solid;
border-radius: 50%;
display: inline-block;
height: 16px;
margin-right: 6px;
width: 16px;
flex-shrink: 0;
box-sizing: border-box;
}
.chartTooltip .bullet {
align-self: baseline;
}
:host([rtl]) .chartLegend .bullet,
:host([rtl]) .chartTooltip .bullet {
margin-right: inherit;
margin-left: 6px;
}
.chartTooltip {
padding: 8px;
font-size: 90%;
position: absolute;
background: rgba(80, 80, 80, 0.9);
color: white;
border-radius: 4px;
pointer-events: none;
z-index: 1000;
width: 200px;
box-sizing: border-box;
}
:host([rtl]) .chartTooltip {
direction: rtl;
}
.chartLegend ul,
.chartTooltip ul {
display: inline-block;
padding: 0 0px;
margin: 8px 0 0 0;
width: 100%;
}
.chartTooltip ul {
margin: 0 4px;
}
.chartTooltip li {
display: flex;
white-space: pre-line;
align-items: center;
line-height: 16px;
padding: 4px 0;
}
.chartTooltip .title {
text-align: center;
font-weight: 500;
}
.chartTooltip .footer {
font-weight: 500;
}
.chartTooltip .beforeBody {
text-align: center;
font-weight: 300;
word-break: break-all;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-chart-base": HaChartBase;
}
}

View File

@@ -0,0 +1,400 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import {
formatNumber,
numberFormatToLocale,
} from "../../common/string/format_number";
import { LineChartEntity, LineChartState } from "../../data/history";
import { HomeAssistant } from "../../types";
import "./ha-chart-base";
const safeParseFloat = (value) => {
const parsed = parseFloat(value);
return isFinite(parsed) ? parsed : null;
};
class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data: LineChartEntity[] = [];
@property() public names: boolean | Record<string, string> = false;
@property() public unit?: string;
@property() public identifier?: string;
@property({ type: Boolean }) public isSingleDevice = false;
@property({ attribute: false }) public endTime?: Date;
@state() private _chartData?: ChartData<"line">;
@state() private _chartOptions?: ChartOptions<"line">;
protected render() {
return html`
<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
chart-type="line"
></ha-chart-base>
`;
}
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this._chartOptions = {
parsing: false,
animation: false,
scales: {
x: {
type: "time",
adapters: {
date: {
locale: this.hass.locale,
},
},
ticks: {
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
time: {
tooltipFormat: "datetimeseconds",
},
},
y: {
ticks: {
maxTicksLimit: 7,
},
title: {
display: true,
text: this.unit,
},
},
},
plugins: {
tooltip: {
mode: "nearest",
callbacks: {
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
this.hass.locale
)} ${this.unit}`,
},
},
filler: {
propagate: true,
},
legend: {
display: !this.isSingleDevice,
labels: {
usePointStyle: true,
},
},
},
hover: {
mode: "nearest",
},
elements: {
line: {
tension: 0.1,
borderWidth: 1.5,
},
point: {
hitRadius: 5,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}
if (changedProps.has("data")) {
this._generateData();
}
}
private _generateData() {
let colorIndex = 0;
const computedStyles = getComputedStyle(this);
const entityStates = this.data;
const datasets: ChartDataset<"line">[] = [];
let endTime: Date;
if (entityStates.length === 0) {
return;
}
endTime =
this.endTime ||
// Get the highest date from the last date of each device
new Date(
Math.max(
...entityStates.map((devSts) =>
new Date(
devSts.states[devSts.states.length - 1].last_changed
).getTime()
)
)
);
if (endTime > new Date()) {
endTime = new Date();
}
const names = this.names || {};
entityStates.forEach((states) => {
const domain = states.domain;
const name = names[states.entity_id] || states.name;
// array containing [value1, value2, etc]
let prevValues: any[] | null = null;
const data: ChartDataset<"line">[] = [];
const pushData = (timestamp: Date, datavalues: any[] | null) => {
if (!datavalues) return;
if (timestamp > endTime) {
// Drop datapoints that are after the requested endTime. This could happen if
// endTime is "now" and client time is not in sync with server time.
return;
}
data.forEach((d, i) => {
if (datavalues[i] === null && prevValues && prevValues[i] !== null) {
// null data values show up as gaps in the chart.
// If the current value for the dataset is null and the previous
// value of the data set is not null, then add an 'end' point
// to the chart for the previous value. Otherwise the gap will
// be too big. It will go from the start of the previous data
// value until the start of the next data value.
d.data.push({ x: timestamp.getTime(), y: prevValues[i] });
}
d.data.push({ x: timestamp.getTime(), y: datavalues[i] });
});
prevValues = datavalues;
};
const addDataSet = (
nameY: string,
step = false,
fill = false,
color?: string
) => {
if (!color) {
color = getColorByIndex(colorIndex);
colorIndex++;
}
data.push({
label: nameY,
fill: fill ? "origin" : false,
borderColor: color,
backgroundColor: color + "7F",
stepped: step ? "before" : false,
pointRadius: 0,
data: [],
});
};
if (
domain === "thermostat" ||
domain === "climate" ||
domain === "water_heater"
) {
const hasHvacAction = states.states.some(
(entityState) => entityState.attributes?.hvac_action
);
const isHeating =
domain === "climate" && hasHvacAction
? (entityState: LineChartState) =>
entityState.attributes?.hvac_action === "heating"
: (entityState: LineChartState) => entityState.state === "heat";
const isCooling =
domain === "climate" && hasHvacAction
? (entityState: LineChartState) =>
entityState.attributes?.hvac_action === "cooling"
: (entityState: LineChartState) => entityState.state === "cool";
const hasHeat = states.states.some(isHeating);
const hasCool = states.states.some(isCooling);
// We differentiate between thermostats that have a target temperature
// range versus ones that have just a target temperature
// Using step chart by step-before so manually interpolation not needed.
const hasTargetRange = states.states.some(
(entityState) =>
entityState.attributes &&
entityState.attributes.target_temp_high !==
entityState.attributes.target_temp_low
);
addDataSet(
`${this.hass.localize("ui.card.climate.current_temperature", {
name: name,
})}`,
true
);
if (hasHeat) {
addDataSet(
`${this.hass.localize("ui.card.climate.heating", { name: name })}`,
true,
true,
computedStyles.getPropertyValue("--state-climate-heat-color")
);
// The "heating" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasCool) {
addDataSet(
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
true,
true,
computedStyles.getPropertyValue("--state-climate-cool-color")
);
// The "cooling" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasTargetRange) {
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.high"),
})}`,
true
);
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.low"),
})}`,
true
);
} else {
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_entity", {
name: name,
})}`,
true
);
}
states.states.forEach((entityState) => {
if (!entityState.attributes) return;
const curTemp = safeParseFloat(
entityState.attributes.current_temperature
);
const series = [curTemp];
if (hasHeat) {
series.push(isHeating(entityState) ? curTemp : null);
}
if (hasCool) {
series.push(isCooling(entityState) ? curTemp : null);
}
if (hasTargetRange) {
const targetHigh = safeParseFloat(
entityState.attributes.target_temp_high
);
const targetLow = safeParseFloat(
entityState.attributes.target_temp_low
);
series.push(targetHigh, targetLow);
pushData(new Date(entityState.last_changed), series);
} else {
const target = safeParseFloat(entityState.attributes.temperature);
series.push(target);
pushData(new Date(entityState.last_changed), series);
}
});
} else if (domain === "humidifier") {
addDataSet(
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
name: name,
})}`,
true
);
addDataSet(
`${this.hass.localize("ui.card.humidifier.on_entity", {
name: name,
})}`,
true,
true
);
states.states.forEach((entityState) => {
if (!entityState.attributes) return;
const target = safeParseFloat(entityState.attributes.humidity);
const series = [target];
series.push(entityState.state === "on" ? target : null);
pushData(new Date(entityState.last_changed), series);
});
} else {
// Only disable interpolation for sensors
const isStep = domain === "sensor";
addDataSet(name, isStep);
let lastValue: number;
let lastDate: Date;
let lastNullDate: Date | null = null;
// Process chart data.
// When state is `unknown`, calculate the value and break the line.
states.states.forEach((entityState) => {
const value = safeParseFloat(entityState.state);
const date = new Date(entityState.last_changed);
if (value !== null && lastNullDate) {
const dateTime = date.getTime();
const lastNullDateTime = lastNullDate.getTime();
const lastDateTime = lastDate?.getTime();
const tmpValue =
(value - lastValue) *
((lastNullDateTime - lastDateTime) /
(dateTime - lastDateTime)) +
lastValue;
pushData(lastNullDate, [tmpValue]);
pushData(new Date(lastNullDateTime + 1), [null]);
pushData(date, [value]);
lastDate = date;
lastValue = value;
lastNullDate = null;
} else if (value !== null && lastNullDate === null) {
pushData(date, [value]);
lastDate = date;
lastValue = value;
} else if (
value === null &&
lastNullDate === null &&
lastValue !== undefined
) {
lastNullDate = date;
}
});
}
// Add an entry for final values
pushData(endTime, prevValues);
// Concat two arrays
Array.prototype.push.apply(datasets, data);
});
this._chartData = {
datasets,
};
}
}
customElements.define("state-history-chart-line", StateHistoryChartLine);
declare global {
interface HTMLElementTagNameMap {
"state-history-chart-line": StateHistoryChartLine;
}
}

View File

@@ -0,0 +1,322 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { computeDomain } from "../../common/entity/compute_domain";
import { numberFormatToLocale } from "../../common/string/format_number";
import { computeRTL } from "../../common/util/compute_rtl";
import { TimelineEntity } from "../../data/history";
import { HomeAssistant } from "../../types";
import "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const";
/** Binary sensor device classes for which the static colors for on/off need to be inverted.
* List the ones were "off" = good or normal state = should be rendered "green".
*/
const BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED = new Set([
"battery",
"door",
"garage_door",
"gas",
"lock",
"opening",
"problem",
"safety",
"smoke",
"window",
]);
const STATIC_STATE_COLORS = new Set([
"on",
"off",
"home",
"not_home",
"unavailable",
"unknown",
"idle",
]);
const stateColorMap: Map<string, string> = new Map();
let colorIndex = 0;
const invertOnOff = (entityState?: HassEntity) =>
entityState &&
computeDomain(entityState.entity_id) === "binary_sensor" &&
"device_class" in entityState.attributes &&
BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED.has(
entityState.attributes.device_class!
);
const getColor = (
stateString: string,
entityState: HassEntity,
computedStyles: CSSStyleDeclaration
) => {
if (invertOnOff(entityState)) {
stateString = stateString === "on" ? "off" : "on";
}
if (stateColorMap.has(stateString)) {
return stateColorMap.get(stateString);
}
if (STATIC_STATE_COLORS.has(stateString)) {
const color = computedStyles.getPropertyValue(
`--state-${stateString}-color`
);
stateColorMap.set(stateString, color);
return color;
}
const color = getColorByIndex(colorIndex);
colorIndex++;
stateColorMap.set(stateString, color);
return color;
};
@customElement("state-history-chart-timeline")
export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data: TimelineEntity[] = [];
@property() public names: boolean | Record<string, string> = false;
@property() public unit?: string;
@property() public identifier?: string;
@property({ type: Boolean }) public isSingleDevice = false;
@property({ attribute: false }) public endTime?: Date;
@state() private _chartData?: ChartData<"timeline">;
@state() private _chartOptions?: ChartOptions<"timeline">;
protected render() {
return html`
<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
.height=${this.data.length * 30 + 30}
chart-type="timeline"
></ha-chart-base>
`;
}
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this._chartOptions = {
maintainAspectRatio: false,
parsing: false,
animation: false,
scales: {
x: {
type: "timeline",
position: "bottom",
adapters: {
date: {
locale: this.hass.locale,
},
},
ticks: {
autoSkip: true,
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
grid: {
offset: false,
},
time: {
tooltipFormat: "datetimeseconds",
},
},
y: {
type: "category",
barThickness: 20,
offset: true,
grid: {
display: false,
drawBorder: false,
drawTicks: false,
},
ticks: {
display: this.data.length !== 1,
},
afterSetDimensions: (y) => {
y.maxWidth = y.chart.width * 0.18;
},
position: computeRTL(this.hass) ? "right" : "left",
},
},
plugins: {
tooltip: {
mode: "nearest",
callbacks: {
title: (context) =>
context![0].chart!.data!.labels![
context[0].datasetIndex
] as string,
beforeBody: (context) => context[0].dataset.label || "",
label: (item) => {
const d = item.dataset.data[item.dataIndex] as TimeLineData;
return [
d.label || "",
formatDateTimeWithSeconds(d.start, this.hass.locale),
formatDateTimeWithSeconds(d.end, this.hass.locale),
];
},
labelColor: (item) => ({
borderColor: (item.dataset.data[item.dataIndex] as TimeLineData)
.color!,
backgroundColor: (
item.dataset.data[item.dataIndex] as TimeLineData
).color!,
}),
},
},
filler: {
propagate: true,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}
if (changedProps.has("data")) {
this._generateData();
}
}
private _generateData() {
const computedStyles = getComputedStyle(this);
let stateHistory = this.data;
if (!stateHistory) {
stateHistory = [];
}
const startTime = new Date(
stateHistory.reduce(
(minTime, stateInfo) =>
Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()),
new Date().getTime()
)
);
// end time is Math.max(startTime, last_event)
let endTime =
this.endTime ||
new Date(
stateHistory.reduce(
(maxTime, stateInfo) =>
Math.max(
maxTime,
new Date(
stateInfo.data[stateInfo.data.length - 1].last_changed
).getTime()
),
startTime.getTime()
)
);
if (endTime > new Date()) {
endTime = new Date();
}
const labels: string[] = [];
const datasets: ChartDataset<"timeline">[] = [];
const names = this.names || {};
// stateHistory is a list of lists of sorted state objects
stateHistory.forEach((stateInfo) => {
let newLastChanged: Date;
let prevState: string | null = null;
let locState: string | null = null;
let prevLastChanged = startTime;
const entityDisplay: string =
names[stateInfo.entity_id] || stateInfo.name;
const dataRow: TimeLineData[] = [];
stateInfo.data.forEach((entityState) => {
let newState: string | null = entityState.state;
const timeStamp = new Date(entityState.last_changed);
if (!newState) {
newState = null;
}
if (timeStamp > endTime) {
// Drop datapoints that are after the requested endTime. This could happen if
// endTime is 'now' and client time is not in sync with server time.
return;
}
if (prevState === null) {
prevState = newState;
locState = entityState.state_localize;
prevLastChanged = new Date(entityState.last_changed);
} else if (newState !== prevState) {
newLastChanged = new Date(entityState.last_changed);
dataRow.push({
start: prevLastChanged,
end: newLastChanged,
label: locState,
color: getColor(
prevState,
this.hass.states[stateInfo.entity_id],
computedStyles
),
});
prevState = newState;
locState = entityState.state_localize;
prevLastChanged = newLastChanged;
}
});
if (prevState !== null) {
dataRow.push({
start: prevLastChanged,
end: endTime,
label: locState,
color: getColor(
prevState,
this.hass.states[stateInfo.entity_id],
computedStyles
),
});
}
datasets.push({
data: dataRow,
label: stateInfo.entity_id,
});
labels.push(entityDisplay);
});
this._chartData = {
labels: labels,
datasets: datasets,
};
}
static get styles(): CSSResultGroup {
return css`
ha-chart-base {
--chart-max-height: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"state-history-chart-timeline": StateHistoryChartTimeline;
}
}

View File

@@ -7,10 +7,9 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { HistoryResult } from "../data/history"; import { HistoryResult } from "../../data/history";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../../types";
import "./ha-circular-progress";
import "./state-history-chart-line"; import "./state-history-chart-line";
import "./state-history-chart-timeline"; import "./state-history-chart-timeline";
@@ -24,7 +23,7 @@ class StateHistoryCharts extends LitElement {
@property({ attribute: false }) public endTime?: Date; @property({ attribute: false }) public endTime?: Date;
@property({ type: Boolean }) public upToNow = false; @property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
@property({ type: Boolean, attribute: "no-single" }) public noSingle = false; @property({ type: Boolean, attribute: "no-single" }) public noSingle = false;
@@ -101,12 +100,12 @@ class StateHistoryCharts extends LitElement {
return css` return css`
:host { :host {
display: block; display: block;
/* height of single timeline chart = 58px */ /* height of single timeline chart = 60px */
min-height: 58px; min-height: 60px;
} }
.info { .info {
text-align: center; text-align: center;
line-height: 58px; line-height: 60px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
`; `;

View File

@@ -0,0 +1,402 @@
import type {
ChartData,
ChartDataset,
ChartOptions,
ChartType,
} from "chart.js";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeStateName } from "../../common/entity/compute_state_name";
import {
formatNumber,
numberFormatToLocale,
} from "../../common/string/format_number";
import {
getStatisticIds,
Statistics,
statisticsHaveType,
StatisticsMetaData,
StatisticType,
} from "../../data/history";
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
@customElement("statistics-chart")
class StatisticsChart extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public statisticsData!: Statistics;
@property({ type: Array }) public statisticIds?: StatisticsMetaData[];
@property() public names: boolean | Record<string, string> = false;
@property() public unit?: string;
@property({ attribute: false }) public endTime?: Date;
@property({ type: Array }) public statTypes: Array<StatisticType> = [
"sum",
"min",
"mean",
"max",
];
@property() public chartType: ChartType = "line";
@property({ type: Boolean }) public isLoadingData = false;
@state() private _chartData: ChartData = { datasets: [] };
@state() private _chartOptions?: ChartOptions;
protected shouldUpdate(changedProps: PropertyValues): boolean {
return changedProps.size > 1 || !changedProps.has("hass");
}
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this._createOptions();
}
if (changedProps.has("statisticsData") || changedProps.has("statTypes")) {
this._generateData();
}
}
protected render(): TemplateResult {
if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info">
${this.hass.localize("ui.components.history_charts.history_disabled")}
</div>`;
}
if (this.isLoadingData && !this.statisticsData) {
return html`<div class="info">
${this.hass.localize(
"ui.components.statistics_charts.loading_statistics"
)}
</div>`;
}
if (!this.statisticsData || !Object.keys(this.statisticsData).length) {
return html`<div class="info">
${this.hass.localize(
"ui.components.statistics_charts.no_statistics_found"
)}
</div>`;
}
return html`
<ha-chart-base
.data=${this._chartData}
.options=${this._chartOptions}
.chartType=${this.chartType}
></ha-chart-base>
`;
}
private _createOptions() {
this._chartOptions = {
parsing: false,
animation: false,
scales: {
x: {
type: "time",
adapters: {
date: {
locale: this.hass.locale,
},
},
ticks: {
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
time: {
tooltipFormat: "datetime",
},
},
y: {
beginAtZero: false,
ticks: {
maxTicksLimit: 7,
},
title: {
display: this.unit,
text: this.unit,
},
},
},
plugins: {
tooltip: {
mode: "nearest",
callbacks: {
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
this.hass.locale
)} ${
// @ts-ignore
context.dataset.unit || ""
}`,
},
},
filler: {
propagate: true,
},
legend: {
display: true,
labels: {
usePointStyle: true,
},
},
},
hover: {
mode: "nearest",
},
elements: {
line: {
tension: 0.4,
borderWidth: 1.5,
},
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 5,
},
},
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
};
}
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass);
}
private async _generateData() {
if (!this.statisticsData) {
return;
}
if (!this.statisticIds) {
await this._getStatisticIds();
}
let colorIndex = 0;
const statisticsData = Object.values(this.statisticsData);
const totalDataSets: ChartDataset<"line">[] = [];
let endTime: Date;
if (statisticsData.length === 0) {
return;
}
endTime =
this.endTime ||
// Get the highest date from the last date of each statistic
new Date(
Math.max(
...statisticsData.map((stats) =>
new Date(stats[stats.length - 1].start).getTime()
)
)
);
if (endTime > new Date()) {
endTime = new Date();
}
let unit: string | undefined | null;
const names = this.names || {};
statisticsData.forEach((stats) => {
const firstStat = stats[0];
let name = names[firstStat.statistic_id];
if (!name) {
const entityState = this.hass.states[firstStat.statistic_id];
if (entityState) {
name = computeStateName(entityState);
} else {
name = firstStat.statistic_id;
}
}
const meta = this.statisticIds!.find(
(stat) => stat.statistic_id === firstStat.statistic_id
);
if (!this.unit) {
if (unit === undefined) {
unit = meta?.unit_of_measurement;
} else if (unit !== meta?.unit_of_measurement) {
unit = null;
}
}
// array containing [value1, value2, etc]
let prevValues: Array<number | null> | null = null;
// The datasets for the current statistic
const statDataSets: ChartDataset<"line">[] = [];
const pushData = (
timestamp: Date,
dataValues: Array<number | null> | null
) => {
if (!dataValues) return;
if (timestamp > endTime) {
// Drop datapoints that are after the requested endTime. This could happen if
// endTime is "now" and client time is not in sync with server time.
return;
}
statDataSets.forEach((d, i) => {
if (dataValues[i] === null && prevValues && prevValues[i] !== null) {
// null data values show up as gaps in the chart.
// If the current value for the dataset is null and the previous
// value of the data set is not null, then add an 'end' point
// to the chart for the previous value. Otherwise the gap will
// be too big. It will go from the start of the previous data
// value until the start of the next data value.
d.data.push({ x: timestamp.getTime(), y: prevValues[i]! });
}
d.data.push({ x: timestamp.getTime(), y: dataValues[i]! });
});
prevValues = dataValues;
};
const color = getColorByIndex(colorIndex);
colorIndex++;
const statTypes: this["statTypes"] = [];
const drawBands =
this.statTypes.includes("mean") && statisticsHaveType(stats, "mean");
const sortedTypes = drawBands
? [...this.statTypes].sort((a, b) => {
if (a === "min" || b === "max") {
return -1;
}
if (a === "max" || b === "min") {
return +1;
}
return 0;
})
: this.statTypes;
sortedTypes.forEach((type) => {
if (statisticsHaveType(stats, type)) {
const band = drawBands && (type === "min" || type === "max");
statTypes.push(type);
statDataSets.push({
label: `${name} (${this.hass.localize(
`ui.components.statistics_charts.statistic_types.${type}`
)})
`,
fill: drawBands
? type === "min"
? "+1"
: type === "max"
? "-1"
: false
: false,
borderColor: band ? color + "7F" : color,
backgroundColor: band ? color + "3F" : color + "7F",
pointRadius: 0,
data: [],
// @ts-ignore
unit: meta?.unit_of_measurement,
band,
});
}
});
let prevDate: Date | null = null;
// Process chart data.
let initVal: number | null = null;
let prevSum: number | null = null;
stats.forEach((stat) => {
const date = new Date(stat.start);
if (prevDate === date) {
return;
}
prevDate = date;
const dataValues: Array<number | null> = [];
statTypes.forEach((type) => {
let val: number | null;
if (type === "sum") {
if (!initVal) {
initVal = val = stat.state;
prevSum = stat.sum;
} else {
val = initVal + ((stat.sum || 0) - prevSum!);
}
} else {
val = stat[type];
}
dataValues.push(val !== null ? Math.round(val * 100) / 100 : null);
});
pushData(date, dataValues);
});
// Add an entry for final values
pushData(endTime, prevValues);
// Concat two arrays
Array.prototype.push.apply(totalDataSets, statDataSets);
});
if (unit !== null) {
this._chartOptions = {
...this._chartOptions,
scales: {
...this._chartOptions!.scales,
y: {
...(this._chartOptions!.scales!.y as Record<string, unknown>),
title: { display: unit, text: unit },
},
},
};
}
this._chartData = {
datasets: totalDataSets,
};
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
min-height: 60px;
}
.info {
text-align: center;
line-height: 60px;
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"statistics-chart": StatisticsChart;
}
}

View File

@@ -0,0 +1,18 @@
export interface TimeLineData {
start: Date;
end: Date;
label?: string | null;
color?: string;
}
declare module "chart.js" {
interface ChartTypeRegistry {
timeline: {
chartOptions: BarControllerChartOptions;
datasetOptions: BarControllerDatasetOptions;
defaultDataPoint: TimeLineData;
parsedDataType: any;
scales: "timeline";
};
}
}

View File

@@ -0,0 +1,59 @@
import { BarElement, BarOptions, BarProps } from "chart.js";
import { hex2rgb } from "../../../common/color/convert-color";
import { luminosity } from "../../../common/color/rgb";
export interface TextBarProps extends BarProps {
text?: string | null;
options?: Partial<TextBaroptions>;
}
export interface TextBaroptions extends BarOptions {
textPad?: number;
textColor?: string;
backgroundColor: string;
}
export class TextBarElement extends BarElement {
static id = "textbar";
draw(ctx) {
super.draw(ctx);
const options = this.options as TextBaroptions;
const { x, y, base, width, text } = (
this as BarElement<TextBarProps, TextBaroptions>
).getProps(["x", "y", "base", "width", "text"]);
if (!text) {
return;
}
ctx.beginPath();
const textRect = ctx.measureText(text);
if (
textRect.width === 0 ||
textRect.width + (options.textPad || 4) + 2 > width
) {
return;
}
const textColor =
options.textColor ||
(options.backgroundColor &&
(luminosity(hex2rgb(options.backgroundColor)) > 0.5 ? "#000" : "#fff"));
// ctx.font = "12px arial";
ctx.fillStyle = textColor;
ctx.lineWidth = 0;
ctx.strokeStyle = textColor;
ctx.textBaseline = "middle";
ctx.fillText(
text,
x - width / 2 + (options.textPad || 4),
y + (base - y) / 2
);
}
tooltipPosition(useFinalPosition: boolean) {
const { x, y, base } = this.getProps(["x", "y", "base"], useFinalPosition);
return { x, y: y + (base - y) / 2 };
}
}

View File

@@ -0,0 +1,160 @@
import { BarController, BarElement } from "chart.js";
import { TimeLineData } from "./const";
import { TextBarProps } from "./textbar-element";
function parseValue(entry, item, vScale, i) {
const startValue = vScale.parse(entry.start, i);
const endValue = vScale.parse(entry.end, i);
const min = Math.min(startValue, endValue);
const max = Math.max(startValue, endValue);
let barStart = min;
let barEnd = max;
if (Math.abs(min) > Math.abs(max)) {
barStart = max;
barEnd = min;
}
// Store `barEnd` (furthest away from origin) as parsed value,
// to make stacking straight forward
item[vScale.axis] = barEnd;
item._custom = {
barStart,
barEnd,
start: startValue,
end: endValue,
min,
max,
};
return item;
}
export class TimelineController extends BarController {
static id = "timeline";
static defaults = {
dataElementType: "textbar",
dataElementOptions: ["text", "textColor", "textPadding"],
elements: {
showText: true,
textPadding: 4,
minBarWidth: 1,
},
layout: {
padding: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
},
};
static overrides = {
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
};
parseObjectData(meta, data, start, count) {
const iScale = meta.iScale;
const vScale = meta.vScale;
const labels = iScale.getLabels();
const singleScale = iScale === vScale;
const parsed: any[] = [];
let i;
let ilen;
let item;
let entry;
for (i = start, ilen = start + count; i < ilen; ++i) {
entry = data[i];
item = {};
item[iScale.axis] = singleScale || iScale.parse(labels[i], i);
parsed.push(parseValue(entry, item, vScale, i));
}
return parsed;
}
getLabelAndValue(index) {
const meta = this._cachedMeta;
const { vScale } = meta;
const data = this.getDataset().data[index] as TimeLineData;
return {
label: vScale!.getLabelForValue(this.index) || "",
value: data.label || "",
};
}
updateElements(
bars: BarElement[],
start: number,
count: number,
mode: "reset" | "resize" | "none" | "hide" | "show" | "normal" | "active"
) {
const vScale = this._cachedMeta.vScale!;
const iScale = this._cachedMeta.iScale!;
const dataset = this.getDataset();
const firstOpts = this.resolveDataElementOptions(start, mode);
const sharedOptions = this.getSharedOptions(firstOpts);
const includeOptions = this.includeOptions(mode, sharedOptions!);
const horizontal = vScale.isHorizontal();
this.updateSharedOptions(sharedOptions!, mode, firstOpts);
for (let index = start; index < start + count; index++) {
const data = dataset.data[index] as TimeLineData;
// @ts-ignore
const y = vScale.getPixelForValue(this.index);
// @ts-ignore
const xStart = iScale.getPixelForValue(data.start.getTime());
// @ts-ignore
const xEnd = iScale.getPixelForValue(data.end.getTime());
const width = xEnd - xStart;
const height = 10;
const properties: TextBarProps = {
horizontal,
x: xStart + width / 2, // Center of the bar
y: y - height, // Top of bar
width,
height: 0,
base: y + height, // Bottom of bar,
// Text
text: data.label,
};
if (includeOptions) {
properties.options =
sharedOptions || this.resolveDataElementOptions(index, mode);
properties.options = {
...properties.options,
backgroundColor: data.color,
};
}
this.updateElement(bars[index], index, properties as any, mode);
}
}
removeHoverStyle(_element, _datasetIndex, _index) {
// this._setStyle(element, index, 'active', false);
}
setHoverStyle(_element, _datasetIndex, _index) {
// this._setStyle(element, index, 'active', true);
}
}

View File

@@ -0,0 +1,55 @@
import { TimeScale } from "chart.js";
import { TimeLineData } from "./const";
export class TimeLineScale extends TimeScale {
static id = "timeline";
static defaults = {
position: "bottom",
tooltips: {
mode: "nearest",
},
ticks: {
autoSkip: true,
},
};
determineDataLimits() {
const options = this.options;
// @ts-ignore
const adapter = this._adapter;
const unit = options.time.unit || "day";
let { min, max } = this.getUserBounds();
const chart = this.chart;
// Convert data to timestamps
chart.data.datasets.forEach((dataset, index) => {
if (!chart.isDatasetVisible(index)) {
return;
}
for (const data of dataset.data as TimeLineData[]) {
let timestamp0 = adapter.parse(data.start, this);
let timestamp1 = adapter.parse(data.end, this);
if (timestamp0 > timestamp1) {
[timestamp0, timestamp1] = [timestamp1, timestamp0];
}
if (min > timestamp0 && timestamp0) {
min = timestamp0;
}
if (max < timestamp1 && timestamp1) {
max = timestamp1;
}
}
});
// In case there is no valid min/max, var's use today limits
min =
isFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit);
max = isFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit);
// Make sure that max is strictly higher than min (required by the lookup table)
this.min = Math.min(min, max - 1);
this.max = Math.max(min + 1, max);
}
}

View File

@@ -0,0 +1,168 @@
export const createCurrencyListEl = () => {
const list = document.createElement("datalist");
list.id = "currencies";
for (const currency of [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMK",
"ZWL",
]) {
const option = document.createElement("option");
option.value = currency;
option.innerHTML = currency;
list.appendChild(option);
}
return list;
};

View File

@@ -1,4 +1,4 @@
import { Layout1d, scroll } from "../../resources/lit-virtualizer"; import { Layout1d, scroll } from "@lit-labs/virtualizer";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import {
css, css,
@@ -10,10 +10,10 @@ import {
} from "lit"; } from "lit";
import { import {
customElement, customElement,
property,
state,
query,
eventOptions, eventOptions,
property,
query,
state,
} from "lit/decorators"; } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
@@ -360,9 +360,8 @@ export class HaDataTable extends LitElement {
.rowId=${row[this.id]} .rowId=${row[this.id]}
@click=${this._handleRowClick} @click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({ class="mdc-data-table__row ${classMap({
"mdc-data-table__row--selected": this._checkedRows.includes( "mdc-data-table__row--selected":
String(row[this.id]) this._checkedRows.includes(String(row[this.id])),
),
clickable: this.clickable, clickable: this.clickable,
})}" })}"
aria-selected=${ifDefined( aria-selected=${ifDefined(
@@ -406,17 +405,15 @@ export class HaDataTable extends LitElement {
"mdc-data-table__cell--icon": Boolean( "mdc-data-table__cell--icon": Boolean(
column.type === "icon" column.type === "icon"
), ),
"mdc-data-table__cell--icon-button": Boolean( "mdc-data-table__cell--icon-button":
column.type === "icon-button" Boolean(column.type === "icon-button"),
),
grows: Boolean(column.grows), grows: Boolean(column.grows),
forceLTR: Boolean(column.forceLTR), forceLTR: Boolean(column.forceLTR),
})}" })}"
style=${column.width style=${column.width
? styleMap({ ? styleMap({
[column.grows [column.grows ? "minWidth" : "width"]:
? "minWidth" column.width,
: "width"]: column.width,
maxWidth: column.maxWidth maxWidth: column.maxWidth
? column.maxWidth ? column.maxWidth
: "", : "",

View File

@@ -1,6 +1,6 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@@ -15,6 +15,7 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@@ -38,7 +39,6 @@ import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./ha-devices-picker"; import "./ha-devices-picker";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
interface DevicesByArea { interface DevicesByArea {
[areaId: string]: AreaDevices; [areaId: string]: AreaDevices;
@@ -52,20 +52,27 @@ interface AreaDevices {
const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style> const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) => html`<style>
paper-item { paper-item {
width: 100%;
margin: -10px 0;
padding: 0; padding: 0;
margin: -10px;
margin-left: 0;
} }
mwc-icon-button { #content {
float: right; display: flex;
align-items: center;
} }
.devices { ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none; display: none;
} }
.devices.visible { :host([selected]) paper-item {
display: block; margin-left: 10px;
} }
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item> <paper-item>
<paper-item-body two-line=""> <paper-item-body two-line="">
<div class="name">${item.name}</div> <div class="name">${item.name}</div>

View File

@@ -11,6 +11,8 @@ import {
} from "lit"; } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { mdiCheck } from "@mdi/js";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { compare } from "../../common/string/compare"; import { compare } from "../../common/string/compare";
@@ -33,7 +35,6 @@ import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-combo-box"; import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
interface Device { interface Device {
name: string; name: string;
@@ -47,10 +48,27 @@ export type HaDevicePickerDeviceFilterFunc = (
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style> const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<style>
paper-item { paper-item {
margin: -10px 0;
padding: 0; padding: 0;
margin: -10px;
margin-left: 0;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-item {
margin-left: 10px;
} }
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item> <paper-item>
<paper-item-body two-line> <paper-item-body two-line>
${item.name} ${item.name}

View File

@@ -1,661 +0,0 @@
/* eslint-plugin-disable lit */
import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import { timeOut } from "@polymer/polymer/lib/utils/async";
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { formatTime } from "../../common/datetime/format_time";
import "../ha-icon-button";
// eslint-disable-next-line no-unused-vars
/* global Chart moment Color */
let scriptsLoaded = null;
class HaChartBase extends mixinBehaviors(
[IronResizableBehavior],
PolymerElement
) {
static get template() {
return html`
<style>
:host {
display: block;
}
.chartHeader {
padding: 6px 0 0 0;
width: 100%;
display: flex;
flex-direction: row;
}
.chartHeader > div {
vertical-align: top;
padding: 0 8px;
}
.chartHeader > div.chartTitle {
padding-top: 8px;
flex: 0 0 0;
max-width: 30%;
}
.chartHeader > div.chartLegend {
flex: 1 1;
min-width: 70%;
}
:root {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.chartTooltip {
font-size: 90%;
opacity: 1;
position: absolute;
background: rgba(80, 80, 80, 0.9);
color: white;
border-radius: 3px;
pointer-events: none;
transform: translate(-50%, 12px);
z-index: 1000;
width: 200px;
transition: opacity 0.15s ease-in-out;
}
:host([rtl]) .chartTooltip {
direction: rtl;
}
.chartLegend ul,
.chartTooltip ul {
display: inline-block;
padding: 0 0px;
margin: 5px 0 0 0;
width: 100%;
}
.chartTooltip ul {
margin: 0 3px;
}
.chartTooltip li {
display: block;
white-space: pre-line;
}
.chartTooltip li::first-line {
line-height: 0;
}
.chartTooltip .title {
text-align: center;
font-weight: 500;
}
.chartTooltip .beforeBody {
text-align: center;
font-weight: 300;
word-break: break-all;
}
.chartLegend li {
display: inline-block;
padding: 0 6px;
max-width: 49%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
}
.chartLegend li:nth-child(odd):last-of-type {
/* Make last item take full width if it is odd-numbered. */
max-width: 100%;
}
.chartLegend li[data-hidden] {
text-decoration: line-through;
}
.chartLegend em,
.chartTooltip em {
border-radius: 5px;
display: inline-block;
height: 10px;
margin-right: 4px;
width: 10px;
}
:host([rtl]) .chartTooltip em {
margin-right: inherit;
margin-left: 4px;
}
ha-icon-button {
color: var(--secondary-text-color);
}
</style>
<template is="dom-if" if="[[unit]]">
<div class="chartHeader">
<div class="chartTitle">[[unit]]</div>
<div class="chartLegend">
<ul>
<template is="dom-repeat" items="[[metas]]">
<li on-click="_legendClick" data-hidden$="[[item.hidden]]">
<em style$="background-color:[[item.bgColor]]"></em>
[[item.label]]
</li>
</template>
</ul>
</div>
</div>
</template>
<div id="chartTarget" style="height:40px; width:100%">
<canvas id="chartCanvas"></canvas>
<div
class$="chartTooltip [[tooltip.yAlign]]"
style$="opacity:[[tooltip.opacity]]; top:[[tooltip.top]]; left:[[tooltip.left]]; padding:[[tooltip.yPadding]]px [[tooltip.xPadding]]px"
>
<div class="title">[[tooltip.title]]</div>
<template is="dom-if" if="[[tooltip.beforeBody]]">
<div class="beforeBody">[[tooltip.beforeBody]]</div>
</template>
<div>
<ul>
<template is="dom-repeat" items="[[tooltip.lines]]">
<li>
<em style$="background-color:[[item.bgColor]]"></em
>[[item.text]]
</li>
</template>
</ul>
</div>
</div>
</div>
`;
}
get chart() {
return this._chart;
}
static get properties() {
return {
data: Object,
identifier: String,
rendered: {
type: Boolean,
notify: true,
value: false,
readOnly: true,
},
metas: {
type: Array,
value: () => [],
},
tooltip: {
type: Object,
value: () => ({
opacity: "0",
left: "0",
top: "0",
xPadding: "5",
yPadding: "3",
}),
},
unit: Object,
rtl: {
type: Boolean,
reflectToAttribute: true,
},
};
}
static get observers() {
return ["onPropsChange(data)"];
}
connectedCallback() {
super.connectedCallback();
this._isAttached = true;
this.onPropsChange();
this._resizeListener = () => {
this._debouncer = Debouncer.debounce(
this._debouncer,
timeOut.after(10),
() => {
if (this._isAttached) {
this.resizeChart();
}
}
);
};
if (typeof ResizeObserver === "function") {
this.resizeObserver = new ResizeObserver((entries) => {
entries.forEach(() => {
this._resizeListener();
});
});
this.resizeObserver.observe(this.$.chartTarget);
} else {
this.addEventListener("iron-resize", this._resizeListener);
}
if (scriptsLoaded === null) {
scriptsLoaded = import("../../resources/ha-chart-scripts.js");
}
scriptsLoaded.then((ChartModule) => {
this.ChartClass = ChartModule.default;
this.onPropsChange();
});
}
disconnectedCallback() {
super.disconnectedCallback();
this._isAttached = false;
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.$.chartTarget);
}
this.removeEventListener("iron-resize", this._resizeListener);
if (this._resizeTimer !== undefined) {
clearInterval(this._resizeTimer);
this._resizeTimer = undefined;
}
}
onPropsChange() {
if (!this._isAttached || !this.ChartClass || !this.data) {
return;
}
this.drawChart();
}
_customTooltips(tooltip) {
// Hide if no tooltip
if (tooltip.opacity === 0) {
this.set(["tooltip", "opacity"], 0);
return;
}
// Set caret Position
if (tooltip.yAlign) {
this.set(["tooltip", "yAlign"], tooltip.yAlign);
} else {
this.set(["tooltip", "yAlign"], "no-transform");
}
const title = tooltip.title ? tooltip.title[0] || "" : "";
this.set(["tooltip", "title"], title);
if (tooltip.beforeBody) {
this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
}
const bodyLines = tooltip.body.map((n) => n.lines);
// Set Text
if (tooltip.body) {
this.set(
["tooltip", "lines"],
bodyLines.map((body, i) => {
const colors = tooltip.labelColors[i];
return {
color: colors.borderColor,
bgColor: colors.backgroundColor,
text: body.join("\n"),
};
})
);
}
const parentWidth = this.$.chartTarget.clientWidth;
let positionX = tooltip.caretX;
const positionY = this._chart.canvas.offsetTop + tooltip.caretY;
if (tooltip.caretX + 100 > parentWidth) {
positionX = parentWidth - 100;
} else if (tooltip.caretX < 100) {
positionX = 100;
}
positionX += this._chart.canvas.offsetLeft;
// Display, position, and set styles for font
this.tooltip = {
...this.tooltip,
opacity: 1,
left: `${positionX}px`,
top: `${positionY}px`,
};
}
_legendClick(event) {
event = event || window.event;
event.stopPropagation();
let target = event.target || event.srcElement;
while (target.nodeName !== "LI") {
// user clicked child, find parent LI
target = target.parentElement;
}
const index = event.model.itemsIndex;
const meta = this._chart.getDatasetMeta(index);
meta.hidden =
meta.hidden === null ? !this._chart.data.datasets[index].hidden : null;
this.set(
["metas", index, "hidden"],
this._chart.isDatasetVisible(index) ? null : "hidden"
);
this._chart.update();
}
_drawLegend() {
const chart = this._chart;
// New data for old graph. Keep metadata.
const preserveVisibility =
this._oldIdentifier && this.identifier === this._oldIdentifier;
this._oldIdentifier = this.identifier;
this.set(
"metas",
this._chart.data.datasets.map((x, i) => ({
label: x.label,
color: x.color,
bgColor: x.backgroundColor,
hidden:
preserveVisibility && i < this.metas.length
? this.metas[i].hidden
: !chart.isDatasetVisible(i),
}))
);
let updateNeeded = false;
if (preserveVisibility) {
for (let i = 0; i < this.metas.length; i++) {
const meta = chart.getDatasetMeta(i);
if (!!meta.hidden !== !!this.metas[i].hidden) updateNeeded = true;
meta.hidden = this.metas[i].hidden ? true : null;
}
}
if (updateNeeded) {
chart.update();
}
this.unit = this.data.unit;
}
_formatTickValue(value, index, values) {
if (values.length === 0) {
return value;
}
const date = new Date(values[index].value);
return formatTime(date, this.hass.locale);
}
drawChart() {
const data = this.data.data;
const ctx = this.$.chartCanvas;
if ((!data.datasets || !data.datasets.length) && !this._chart) {
return;
}
if (this.data.type !== "timeline" && data.datasets.length > 0) {
const cnt = data.datasets.length;
const colors = this.constructor.getColorList(cnt);
for (let loopI = 0; loopI < cnt; loopI++) {
data.datasets[loopI].borderColor = colors[loopI].rgbString();
data.datasets[loopI].backgroundColor = colors[loopI]
.alpha(0.6)
.rgbaString();
}
}
if (this._chart) {
this._customTooltips({ opacity: 0 });
this._chart.data = data;
this._chart.update({ duration: 0 });
if (this.isTimeline) {
this._chart.options.scales.yAxes[0].gridLines.display = data.length > 1;
} else if (this.data.legend === true) {
this._drawLegend();
}
this.resizeChart();
} else {
if (!data.datasets) {
return;
}
this._customTooltips({ opacity: 0 });
const plugins = [{ afterRender: () => this._setRendered(true) }];
let options = {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 0,
},
hover: {
animationDuration: 0,
},
responsiveAnimationDuration: 0,
tooltips: {
enabled: false,
custom: this._customTooltips.bind(this),
},
legend: {
display: false,
},
line: {
spanGaps: true,
},
elements: {
font: "12px 'Roboto', 'sans-serif'",
},
ticks: {
fontFamily: "'Roboto', 'sans-serif'",
},
};
options = Chart.helpers.merge(options, this.data.options);
options.scales.xAxes[0].ticks.callback = this._formatTickValue.bind(this);
if (this.data.type === "timeline") {
this.set("isTimeline", true);
if (this.data.colors !== undefined) {
this._colorFunc = this.constructor.getColorGenerator(
this.data.colors.staticColors,
this.data.colors.staticColorIndex
);
}
if (this._colorFunc !== undefined) {
options.elements.colorFunction = this._colorFunc;
}
if (data.datasets.length === 1) {
if (options.scales.yAxes[0].ticks) {
options.scales.yAxes[0].ticks.display = false;
} else {
options.scales.yAxes[0].ticks = { display: false };
}
if (options.scales.yAxes[0].gridLines) {
options.scales.yAxes[0].gridLines.display = false;
} else {
options.scales.yAxes[0].gridLines = { display: false };
}
}
this.$.chartTarget.style.height = "50px";
} else {
this.$.chartTarget.style.height = "160px";
}
const chartData = {
type: this.data.type,
data: this.data.data,
options: options,
plugins: plugins,
};
// Async resize after dom update
this._chart = new this.ChartClass(ctx, chartData);
if (this.isTimeline !== true && this.data.legend === true) {
this._drawLegend();
}
this.resizeChart();
}
}
resizeChart() {
if (!this._chart) return;
// Chart not ready
if (this._resizeTimer === undefined) {
this._resizeTimer = setInterval(this.resizeChart.bind(this), 10);
return;
}
clearInterval(this._resizeTimer);
this._resizeTimer = undefined;
this._resizeChart();
}
_resizeChart() {
const chartTarget = this.$.chartTarget;
const options = this.data;
const data = options.data;
if (data.datasets.length === 0) {
return;
}
if (!this.isTimeline) {
this._chart.resize();
return;
}
// Recalculate chart height for Timeline chart
const areaTop = this._chart.chartArea.top;
const areaBot = this._chart.chartArea.bottom;
const height1 = this._chart.canvas.clientHeight;
if (areaBot > 0) {
this._axisHeight = height1 - areaBot + areaTop;
}
if (!this._axisHeight) {
chartTarget.style.height = "50px";
this._chart.resize();
this.resizeChart();
return;
}
if (this._axisHeight) {
const cnt = data.datasets.length;
const targetHeight = 30 * cnt + this._axisHeight + "px";
if (chartTarget.style.height !== targetHeight) {
chartTarget.style.height = targetHeight;
}
this._chart.resize();
}
}
// Get HSL distributed color list
static getColorList(count) {
let processL = false;
if (count > 10) {
processL = true;
count = Math.ceil(count / 2);
}
const h1 = 360 / count;
const result = [];
for (let loopI = 0; loopI < count; loopI++) {
result[loopI] = Color().hsl(h1 * loopI, 80, 38);
if (processL) {
result[loopI + count] = Color().hsl(h1 * loopI, 80, 62);
}
}
return result;
}
static getColorGenerator(staticColors, startIndex) {
// Known colors for static data,
// should add for very common state string manually.
// Palette modified from http://google.github.io/palette.js/ mpn65, Apache 2.0
const palette = [
"ff0029",
"66a61e",
"377eb8",
"984ea3",
"00d2d5",
"ff7f00",
"af8d00",
"7f80cd",
"b3e900",
"c42e60",
"a65628",
"f781bf",
"8dd3c7",
"bebada",
"fb8072",
"80b1d3",
"fdb462",
"fccde5",
"bc80bd",
"ffed6f",
"c4eaff",
"cf8c00",
"1b9e77",
"d95f02",
"e7298a",
"e6ab02",
"a6761d",
"0097ff",
"00d067",
"f43600",
"4ba93b",
"5779bb",
"927acc",
"97ee3f",
"bf3947",
"9f5b00",
"f48758",
"8caed6",
"f2b94f",
"eff26e",
"e43872",
"d9b100",
"9d7a00",
"698cff",
"d9d9d9",
"00d27e",
"d06800",
"009f82",
"c49200",
"cbe8ff",
"fecddf",
"c27eb6",
"8cd2ce",
"c4b8d9",
"f883b0",
"a49100",
"f48800",
"27d0df",
"a04a9b",
];
function getColorIndex(idx) {
// Reuse the color if index too large.
return Color("#" + palette[idx % palette.length]);
}
const colorDict = {};
let colorIndex = 0;
if (startIndex > 0) colorIndex = startIndex;
if (staticColors) {
Object.keys(staticColors).forEach((c) => {
const c1 = staticColors[c];
if (isFinite(c1)) {
colorDict[c.toLowerCase()] = getColorIndex(c1);
} else {
colorDict[c.toLowerCase()] = Color(staticColors[c]);
}
});
}
// Custom color assign
function getColor(__, data) {
let ret;
const name = data[3];
if (name === null) return Color().hsl(0, 40, 38);
if (name === undefined) return Color().hsl(120, 40, 38);
let name1 = name.toLowerCase();
if (ret === undefined) {
if (data[4]) {
// Invert on/off if data[4] is true. Required for some binary_sensor device classes
// (BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED) where "off" is the good (= green color) value.
name1 = name1 === "on" ? "off" : name1 === "off" ? "on" : name1;
}
ret = colorDict[name1];
}
if (ret === undefined) {
ret = getColorIndex(colorIndex);
colorIndex++;
colorDict[name1] = ret;
}
return ret;
}
return getColor;
}
}
customElements.define("ha-chart-base", HaChartBase);

View File

@@ -12,7 +12,7 @@ import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
class HaEntitiesPickerLight extends LitElement { class HaEntitiesPickerLight extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property() public value?: string[]; @property({ type: Array }) public value?: string[];
/** /**
* Show entities from specific domains. * Show entities from specific domains.
@@ -30,6 +30,22 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array, attribute: "exclude-domains" }) @property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[]; public excludeDomains?: string[];
/**
* Show only entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
/**
* Show only entities with these unit of measuments.
* @type {Array}
* @attr include-unit-of-measurement
*/
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
@property({ attribute: "picked-entity-label" }) @property({ attribute: "picked-entity-label" })
public pickedEntityLabel?: string; public pickedEntityLabel?: string;
@@ -51,6 +67,8 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains} .excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter} .entityFilter=${this._entityFilter}
.value=${entityId} .value=${entityId}
.label=${this.pickedEntityLabel} .label=${this.pickedEntityLabel}
@@ -64,6 +82,8 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains} .excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter} .entityFilter=${this._entityFilter}
.label=${this.pickEntityLabel} .label=${this.pickEntityLabel}
@value-changed=${this._addEntity} @value-changed=${this._addEntity}
@@ -81,11 +101,11 @@ class HaEntitiesPickerLight extends LitElement {
} }
private async _updateEntities(entities) { private async _updateEntities(entities) {
this.value = entities;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: entities, value: entities,
}); });
this.value = entities;
} }
private _entityChanged(event: PolymerChangedEvent<string>) { private _entityChanged(event: PolymerChangedEvent<string>) {
@@ -98,20 +118,22 @@ class HaEntitiesPickerLight extends LitElement {
) { ) {
return; return;
} }
if (newValue === "") { const currentEntities = this._currentEntities;
this._updateEntities( if (!newValue || currentEntities.includes(newValue)) {
this._currentEntities.filter((ent) => ent !== curValue) this._updateEntities(currentEntities.filter((ent) => ent !== curValue));
); return;
} else {
this._updateEntities(
this._currentEntities.map((ent) => (ent === curValue ? newValue : ent))
);
} }
this._updateEntities(
currentEntities.map((ent) => (ent === curValue ? newValue : ent))
);
} }
private async _addEntity(event: PolymerChangedEvent<string>) { private async _addEntity(event: PolymerChangedEvent<string>) {
event.stopPropagation(); event.stopPropagation();
const toAdd = event.detail.value; const toAdd = event.detail.value;
if (!toAdd) {
return;
}
(event.currentTarget as any).value = ""; (event.currentTarget as any).value = "";
if (!toAdd) { if (!toAdd) {
return; return;

View File

@@ -1,5 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
@@ -25,10 +25,27 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style> const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
paper-item { paper-item {
margin: -5px -10px;
padding: 0; padding: 0;
margin: -10px;
margin-left: 0;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
margin-right: -2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-item {
margin-left: 10px;
} }
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-item>${formatAttributeName(item)}</paper-item>`; <paper-item>${formatAttributeName(item)}</paper-item>`;
@customElement("ha-entity-attribute-picker") @customElement("ha-entity-attribute-picker")

View File

@@ -1,5 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@@ -28,10 +28,25 @@ export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style> const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
paper-icon-item { paper-icon-item {
margin: -10px;
padding: 0; padding: 0;
margin: -8px;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
} }
</style> </style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-icon-item> <paper-icon-item>
<state-badge slot="item-icon" .stateObj=${item}></state-badge> <state-badge slot="item-icon" .stateObj=${item}></state-badge>
<paper-item-body two-line=""> <paper-item-body two-line="">
@@ -42,6 +57,8 @@ const rowRenderer: ComboBoxLitRenderer<HassEntity> = (item) => html`<style>
@customElement("ha-entity-picker") @customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement { export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public autofocus = false; @property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled?: boolean; @property({ type: Boolean }) public disabled?: boolean;
@@ -49,8 +66,6 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean, attribute: "allow-custom-entity" }) @property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity; public allowCustomEntity;
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public label?: string; @property() public label?: string;
@property() public value?: string; @property() public value?: string;
@@ -79,6 +94,14 @@ export class HaEntityPicker extends LitElement {
@property({ type: Array, attribute: "include-device-classes" }) @property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[]; public includeDeviceClasses?: string[];
/**
* Show only entities with these unit of measuments.
* @type {Array}
* @attr include-unit-of-measurement
*/
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
@property() public entityFilter?: HaEntityPickerEntityFilterFunc; @property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) public hideClearIcon = false; @property({ type: Boolean }) public hideClearIcon = false;
@@ -110,7 +133,8 @@ export class HaEntityPicker extends LitElement {
includeDomains: this["includeDomains"], includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"], excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"], entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"] includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
) => { ) => {
let states: HassEntity[] = []; let states: HassEntity[] = [];
@@ -143,6 +167,18 @@ export class HaEntityPicker extends LitElement {
); );
} }
if (includeUnitOfMeasurement) {
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
stateObj.entity_id === this.value ||
(stateObj.attributes.unit_of_measurement &&
includeUnitOfMeasurement.includes(
stateObj.attributes.unit_of_measurement
))
);
}
if (entityFilter) { if (entityFilter) {
states = states.filter( states = states.filter(
(stateObj) => (stateObj) =>
@@ -184,7 +220,7 @@ export class HaEntityPicker extends LitElement {
return !(!changedProps.has("_opened") && this._opened); return !(!changedProps.has("_opened") && this._opened);
} }
protected updated(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) { if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
this._states = this._getStates( this._states = this._getStates(
this._opened, this._opened,
@@ -192,23 +228,24 @@ export class HaEntityPicker extends LitElement {
this.includeDomains, this.includeDomains,
this.excludeDomains, this.excludeDomains,
this.entityFilter, this.entityFilter,
this.includeDeviceClasses this.includeDeviceClasses,
this.includeUnitOfMeasurement
); );
(this.comboBox as any).filteredItems = this._states; if (this._initedStates) {
(this.comboBox as any).filteredItems = this._states;
}
this._initedStates = true; this._initedStates = true;
} }
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html` return html`
<vaadin-combo-box-light <vaadin-combo-box-light
item-value-path="entity_id" item-value-path="entity_id"
item-label-path="entity_id" item-label-path="entity_id"
.value=${this._value} .value=${this._value}
.allowCustomValue=${this.allowCustomEntity} .allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
${comboBoxRenderer(rowRenderer)} ${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}

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