Compare commits

...

196 Commits

Author SHA1 Message Date
Bram Kragten
35496ead23 20230906.1 (#17838) 2023-09-06 13:47:02 +02:00
Bram Kragten
ba88fef09b Bumped version to 20230906.1 2023-09-06 13:41:40 +02:00
Paul Bottein
ad0e59c8f4 Fix no listener margin in flow preview template (#17837) 2023-09-06 13:41:16 +02:00
Bram Kragten
14e6f5e8ca Subscribe to config entries in helper config (#17835) 2023-09-06 13:39:20 +02:00
Bram Kragten
41403a5d35 20230906.0 (#17834) 2023-09-06 09:54:26 +02:00
Bram Kragten
3c48157793 Use report errors instead of strict for template subscription (#17824) 2023-09-06 09:53:54 +02:00
Bram Kragten
3a07af6ad2 Bumped version to 20230906.0 2023-09-06 09:42:29 +02:00
Bram Kragten
c1c05f8d22 Add listeners to template helper preview (#17833) 2023-09-06 09:37:38 +02:00
Bram Kragten
29aed5371c Move translation fetching to gulp action (#17827) 2023-09-06 00:28:26 +02:00
Bram Kragten
76c878df57 Report Unhandled promise rejection as debug for now (#17831) 2023-09-05 16:39:40 -04:00
Bram Kragten
8acf557137 20230905.0 (#17828) 2023-09-05 18:11:08 +02:00
Bram Kragten
d6e7ebe71d Bumped version to 20230905.0 2023-09-05 18:07:59 +02:00
Erik Montnemery
085b26d5ea Simplify entity sources (#17770) 2023-09-05 14:45:22 +02:00
Bram Kragten
32472ca627 Add weather forecast options to more info (#17823) 2023-09-05 13:29:56 +02:00
Bram Kragten
c3c4bb4421 Fix initial value of select selector (#17822) 2023-09-05 13:29:47 +02:00
Paul Bottein
f7f1a0c32d Add better localize keys typings for config pages (#17815)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-09-05 11:39:03 +02:00
Bram Kragten
d4872b177f Start improve restore backup in onboarding (#17813) 2023-09-04 20:08:33 +02:00
Simon Lamon
5bb8c51d25 Guard against empty application credential domain (#17786) 2023-09-04 19:21:56 +02:00
Bram Kragten
4e62370d18 20230904.0 (#17819) 2023-09-04 19:18:58 +02:00
Bram Kragten
77c08fd00f Bumped version to 20230904.0 2023-09-04 19:18:08 +02:00
Bram Kragten
d8894a0078 Don't report errors while not connected (#17818) 2023-09-04 19:16:11 +02:00
renovate[bot]
4fd9c63633 Update dependency hls.js to v1.4.12 (#17817)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 15:31:48 +00:00
Paul Bottein
5e1583f925 Add keyboard support to onboarding links (#17816) 2023-09-04 17:20:51 +02:00
renovate[bot]
5d5894cae6 Update dependency @babel/preset-env to v7.22.14 (#17791)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 14:59:44 +02:00
renovate[bot]
5417513f49 Update dependency hls.js to v1.4.11 (#17809)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 14:59:07 +02:00
renovate[bot]
546ba8f12f Update dependency @codemirror/view to v6.17.1 (#17760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 14:58:32 +02:00
Bram Kragten
a398b37380 Fix combobox selecting wrong item on iOS (#17812) 2023-09-04 12:39:22 +02:00
Kendell R
321f35f30e Fix hide disabled entities button (#17806)
fix hide disabled entities button
2023-09-04 11:49:05 +02:00
renovate[bot]
82dfb06a04 Update dependency @lit-labs/virtualizer to v2.0.7 (#17794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-03 16:27:39 -04:00
c0ffeeca7
e666aac1bd Fix typo (#17803) 2023-09-03 11:34:54 -04:00
renovate[bot]
9e9a0e377e Update dependency glob to v10.3.4 (#17802) 2023-09-03 11:19:50 -04:00
renovate[bot]
ba3f9a318b Update dependency @types/qrcode to v1.5.2 (#17799) 2023-09-03 11:14:41 -04:00
renovate[bot]
f3b4eefb72 Update dependency @types/luxon to v3.3.2 (#17798) 2023-09-03 15:02:08 +00:00
renovate[bot]
6ac1db6953 Update dependency @types/ua-parser-js to v0.7.37 (#17800) 2023-09-03 10:54:11 -04:00
renovate[bot]
1b42189dd6 Update dependency @types/leaflet-draw to v1.0.8 (#17797) 2023-09-03 10:51:01 -04:00
renovate[bot]
0d893b3d2b Update dependency del to v7.1.0 (#17792) 2023-09-02 18:29:33 -04:00
renovate[bot]
7b167a4d7e Update dependency @types/leaflet to v1.9.4 (#17795) 2023-09-02 18:26:28 -04:00
renovate[bot]
8e2f1026e7 Update dependency prettier to v3.0.3 (#17776)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-01 14:22:35 -04:00
Paul Bottein
fe3a63af80 Sort data using locale in data tables (#17781)
* Sort data using locale in data tables

* Only use string compare is both values are string
2023-09-01 17:33:41 +02:00
Bram Kragten
f90ab60354 20230901.0 (#17782) 2023-09-01 17:02:38 +02:00
Bram Kragten
5da4e1860a Rename CO2 signal -> Electricity Map 2023-09-01 17:02:14 +02:00
Bram Kragten
6dcb7f2273 Hide google translate in onboarding 2023-09-01 16:53:14 +02:00
Bram Kragten
53ae7e5a0c Bumped version to 20230901.0 2023-09-01 16:42:30 +02:00
Bram Kragten
56381f9914 Show helper config entries without entities (#17780) 2023-09-01 14:38:56 +00:00
Paul Bottein
be31aecf00 Use right supported features for tilt position in editor (#17779) 2023-09-01 15:54:35 +02:00
Paul Bottein
cc5fffc174 Use state icon for helpers (#17777) 2023-09-01 15:54:19 +02:00
Bram Kragten
dd8a50af31 Add state color to preview row (#17778) 2023-09-01 15:45:32 +02:00
Paul Bottein
c8feded4f2 Use format entity state helpers everywhere (#17757) 2023-09-01 15:26:46 +02:00
Paul Bottein
0d0fe75f4e Use slider for color temp instead of wheel (#17771) 2023-09-01 15:25:27 +02:00
Paul Bottein
fb69deb617 Fix target range temperature in tile feature (#17772) 2023-09-01 15:24:36 +02:00
Paul Bottein
c291af5d97 Improve disabled state for select and number button control (#17773) 2023-09-01 15:19:28 +02:00
Paul Bottein
6d63028406 Move tile card features logic into its own file (#17775) 2023-09-01 15:18:47 +02:00
karwosts
3917739ad2 Fix chart tooltip crash for disabled entity (#17767) 2023-09-01 11:36:43 +02:00
renovate[bot]
e98e59a265 Update typescript-eslint monorepo to v6.5.0 (#17762)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-31 19:40:36 -04:00
renovate[bot]
16ed60902d Update dependency @types/sortablejs to v1.15.2 (#17761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-31 19:35:19 -04:00
Paul Bottein
6c7efc17c2 Fix more info type test (#17758) 2023-08-31 22:37:25 +02:00
Bram Kragten
d187aa0ac6 20230831.0 (#17756) 2023-08-31 14:38:24 +02:00
Bram Kragten
9c60a047c1 Bumped version to 20230831.0 2023-08-31 14:33:00 +02:00
Bram Kragten
1825749036 Get forecasts from large to small (#17755) 2023-08-31 14:32:30 +02:00
Bram Kragten
93846a2867 Fix unit and currency, always select first result of search (#17753)
* Fix unit and currency, always select first result of search

* Update onboarding-location.ts
2023-08-31 14:23:33 +02:00
Bram Kragten
f3ed0160af Fix tooltip error for climate charts (#17752) 2023-08-31 14:22:39 +02:00
Bram Kragten
38b275f7f9 Dont resize chart-timeline (#17751) 2023-08-31 14:21:35 +02:00
Bram Kragten
c3a36efaa4 Fix device energy graphs (#17754) 2023-08-31 14:20:57 +02:00
Paul Bottein
68fa67e77a Use entity translation for away mode (#17750)
* Use entity translation for away mode

* Remove keys
2023-08-31 14:19:40 +02:00
Paul Bottein
806cebb024 Fix more info translation keys (#17749) 2023-08-31 11:06:29 +00:00
Paul Bottein
fa788a8223 Use heat or cool slider mode for auto if there is only one mode (#17748) 2023-08-31 13:00:58 +02:00
Paul Bottein
dfbaee1649 Fix margin between buttons row and attributes in more info (#17738) 2023-08-31 11:02:04 +02:00
karwosts
cfb698d0a6 Fix more info weather forecast (#17747) 2023-08-31 09:42:57 +02:00
Simon Lamon
63c3d6406d Remove Polymer app layout (#17739)
* Remove legacy app layout

* Remove left over ha-app-layout import
2023-08-30 12:24:31 -04:00
Raman Gupta
d817e92a57 Remove node status from zwave_js device info card (#17732) 2023-08-30 15:52:11 +02:00
Bram Kragten
96597b3963 20230830.0 (#17737) 2023-08-30 15:48:10 +02:00
Bram Kragten
40c7bc08d9 Merge branch 'master' into dev 2023-08-30 15:29:55 +02:00
Bram Kragten
b8cd1760f7 Bumped version to 20230830.0 2023-08-30 15:27:57 +02:00
Bram Kragten
24dd45c8cd Bump ChartJS to version 4 (#15531) 2023-08-30 15:27:24 +02:00
Simon Lamon
e06bd41b5e Migrate mailbox to LitElement (#17690)
* migrate mailbox to lit

* Make some methods private

* Clean up
2023-08-30 15:21:20 +02:00
Bram Kragten
c0793fad83 Listen to change event on string form too for autofill (#17733)
* Listen to change event on string form too for autofill

* Update ha-password-manager-polyfill.ts
2023-08-30 14:58:38 +02:00
karwosts
e002c5d96c Allow theme colors for individual sets in energy-usage-graph-card (#17527)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-08-30 13:38:38 +02:00
karwosts
099e317d17 Fix zone dialog rendering wider than the window (#17642) 2023-08-30 13:37:50 +02:00
dependabot[bot]
ca1a183512 Bump actions/checkout from 3.5.3 to 3.6.0 (#17723)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 13:31:09 +02:00
renovate[bot]
c1cacf735e Update dependency eslint to v8.48.0 (#17726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-30 13:30:23 +02:00
renovate[bot]
515cfdb6d1 Update dependency cropperjs to v1.6.0 (#17728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-30 13:29:56 +02:00
renovate[bot]
3a6cffd6c1 Update CodeMirror (#17730)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-30 13:29:02 +02:00
renovate[bot]
c84a826937 Update dependency marked to v7.0.5 (#17731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-30 13:28:35 +02:00
Bram Kragten
c485e8d03e Update onboarding (#17734)
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-08-30 13:24:25 +02:00
Bram Kragten
2ab67328d4 Update upload element (#17654) 2023-08-30 13:24:11 +02:00
Bram Kragten
d350c35c4e Add preview for template (#17699) 2023-08-30 12:56:53 +02:00
karwosts
034ce56da5 Updates to alarm panel card configuration (#17598)
* Updates to alarm panel card configuration

* changes from feedback
2023-08-30 11:42:51 +02:00
karwosts
ae9fcebfd5 Add sortable options to input_select settings menu (#17706)
* Sortable options in input_select settings menu

* fix lint

* no ripple, default cursor

* Update src/panels/config/helpers/forms/ha-input_select-form.ts

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* sortableStyles

* Use ha-list-item and mwc-list

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-08-30 07:59:06 +00:00
renovate[bot]
6197b55da8 Update dependency luxon to v3.4.2 (#17710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-30 09:54:39 +02:00
Bram Kragten
4e5d57b5f3 Ask user to logout all devices when changing password (#17523)
* Ask user to logout all devices when changing password

* missing import

* use core command
2023-08-29 17:09:17 +02:00
Paul Bottein
7040c6d469 Add target temperature tile feature for climate and water heater (#17697) 2023-08-29 16:36:07 +02:00
Paul Bottein
6f99a39b55 Adapt more info button layout depending of number of items and screen (#17691) 2023-08-29 15:13:08 +02:00
Paul Bottein
7483833dcd Use active color for position cover tile feature even if it's closed (#17685) 2023-08-29 15:11:30 +02:00
Sam Reed
38fb48b231 Add trailing full stop to visit_addon_page message (#17719) 2023-08-29 14:38:15 +02:00
karwosts
166acee1c6 Fix language picker in profile displaying wrong language (#17725)
* fix language picker when changing language sort order #16642

* alternative fix
2023-08-29 09:45:54 +02:00
renovate[bot]
916a6df39b Update vaadinWebComponents monorepo to v24.1.6 (#17724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-28 18:03:02 +02:00
renovate[bot]
70f37158fb Update dependency @material/web to v1.0.0-pre.16 (#17703)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-28 18:01:28 +02:00
renovate[bot]
5011bba20e Update dependency typescript to v5.2.2 (#17720)
* Update dependency typescript to v5.2.2

* Remove expect error for URL.canParse

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-08-27 17:43:49 +00:00
renovate[bot]
8897bc703d Update babel monorepo to v7.22.11 (#17717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-27 13:15:40 -04:00
renovate[bot]
ea6e7d441a Update dependency chai to v4.3.8 (#17718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-27 13:10:52 -04:00
renovate[bot]
f91396c986 Update Yarn to v3.6.3 (#17715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-27 13:09:28 -04:00
renovate[bot]
4598b530af Update dependency @lit-labs/virtualizer to v2.0.6 (#17702)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-26 13:14:25 -04:00
renovate[bot]
dfabb4bc36 Update dependency @rollup/plugin-node-resolve to v15.2.1 (#17704) 2023-08-25 21:18:32 -04:00
Bram Kragten
66e0100c95 Fix combobox picking first item with label on blur (#17701) 2023-08-24 16:22:26 -04:00
renovate[bot]
a08a989ef5 Update dependency lint-staged to v14.0.1 (#17694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-24 15:18:37 -04:00
renovate[bot]
000c28abf9 Update dependency magic-string to v0.30.3 (#17695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-24 15:16:40 -04:00
renovate[bot]
6b67397c83 Update typescript-eslint monorepo to v6.4.1 (#17700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-24 15:15:50 -04:00
karwosts
f68823a09e Add for_each to repeat action UI. Convert repeat to ha-form (#17688)
* Add for_each to repeat action UI. Convert repeat to ha-form

* reordermode as a selector option

* css styling
2023-08-24 18:13:10 +02:00
Bram Kragten
fc1782e676 Use 1 element for all group previews (#17693) 2023-08-24 10:14:30 +00:00
Simon Lamon
b4975344a1 Don't show a battery if the entity domain is Number (#17631) 2023-08-24 11:56:03 +02:00
Joakim Sørensen
2dc08d782f Hide STT and TTS entities from generated dashboard (#17689) 2023-08-24 08:59:41 +02:00
Erik Montnemery
ed92958735 Add preview support to binary sensor group (#17682) 2023-08-23 14:52:45 +02:00
Paul Bottein
5ce31f3177 Adapt circular slider style for climate, water_heater and humidifier (#17677)
* Add more rendering mode for circular slider

* Improve transitions
2023-08-23 14:35:54 +02:00
Bram Kragten
370ec9cd98 Revert "Simplify onboarding integrations page (#17671)" (#17683) 2023-08-23 14:28:34 +02:00
Bram Kragten
52c12b5659 Simplify onboarding integrations page (#17671) 2023-08-23 10:52:44 +02:00
renovate[bot]
3de4cfbc00 Update dependency marked to v7.0.4 (#17680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 10:14:39 +02:00
renovate[bot]
4215854414 Update dependency eslint-import-resolver-webpack to v0.13.7 (#17675)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 10:14:12 +02:00
Simon Lamon
88eba92f57 Migrate ha-service-call-button to LitElement (#17666) 2023-08-22 19:49:30 +02:00
Matthias Alphart
f773c968f9 Clear input element value of ha-file-upload (#17663) 2023-08-22 17:21:06 +02:00
Simon Lamon
bbb6fccaec Clean system health subscription after data is collected (#17665) 2023-08-22 17:20:13 +02:00
karwosts
aa2b2b0d16 Typo: lates -> latest (#17673) 2023-08-22 17:14:10 +02:00
Simon Lamon
5cc06ebf0b Migrate gallery pages to LitElement (#17667) 2023-08-22 17:10:26 +02:00
Paul Bottein
85977e505b Add cover tilt position to tile card (#17619)
Add cover tilt to tile card
2023-08-22 16:55:12 +02:00
Bram Kragten
3249a5225f Keep user signed in during onboarding (#17669) 2023-08-22 15:26:46 +02:00
Paul Bottein
7e7205627a Update return home and dock icon (#17672)
Update return home and docker icon
2023-08-22 15:24:30 +02:00
Paul Bottein
d33430e53f Reduce ha-icon-button-group height (#17664) 2023-08-22 11:09:18 +02:00
Bram Kragten
e3f53e90e2 Fix not showing multi day events on mobile (#17660) 2023-08-22 11:07:46 +02:00
Bram Kragten
811edfcc0f Add support for previews in data flows (#17533) 2023-08-22 10:30:31 +02:00
renovate[bot]
2483249b5f Update dependency eslint-plugin-import to v2.28.1 (#17661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-21 18:57:19 -04:00
Bram Kragten
3534617f81 Fix initial data for select selector without labels (#17659) 2023-08-21 21:46:08 +02:00
renovate[bot]
216a3c4c7e Update dependency core-js to v3.32.1 (#17658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-21 20:08:43 +02:00
Paul Bottein
567bd9831f Reduce control select menu component size (#17657) 2023-08-21 16:15:29 +02:00
Bram Kragten
98d1a55d35 fix tabindex for glance card (#17656) 2023-08-21 16:15:08 +02:00
karwosts
92358b4859 Don't show pointer for glance entity when there is no action (#17625) 2023-08-21 15:51:33 +02:00
Paul Bottein
eca3ec7f98 Tile feature lawn mower (#17655)
* Add lawn mower commands to tile card

* Rename state to action button
2023-08-21 15:49:33 +02:00
Simon Lamon
bfcdbbd70b Form fields should not init in the error state (#17615) 2023-08-21 15:49:27 +02:00
karwosts
e764076b1a Fix blueprint editor behavior for number and text with defaults (#17646) 2023-08-21 15:48:10 +02:00
Virenbar
693c77ce1c Fix AND condition description (#17630) 2023-08-21 13:41:26 +00:00
Simon Lamon
a725b6c9de Clear entityId before duplicate script (#17624) 2023-08-21 15:27:54 +02:00
ildar170975
014bbf12ce Update ha-config-person.ts: fix misaligned text (#17637) 2023-08-21 15:12:58 +02:00
ildar170975
07dceb8e6d Update ha-sidebar.ts: fix notification badge clipped (#17638) 2023-08-21 15:12:13 +02:00
karwosts
53f18bec53 Respect sensor precision setting when rendering line chart tooltips (#17648) 2023-08-21 15:04:23 +02:00
renovate[bot]
ac3e858738 Update dependency eslint-plugin-lit to v1.9.1 (#17556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-21 13:46:07 +02:00
Erik Montnemery
c76b2fb357 Fix reselecting forecast type in weather forecast card editor (#17652)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-08-21 11:22:01 +00:00
Paul Bottein
5f015ac9af Add basic more info for lawn mower (#17601)
* Add basic more info for lawn mower

* Change buttons layout
2023-08-21 13:17:11 +02:00
renovate[bot]
ac7c354bfc Update dependency @lrnwebcomponents/simple-tooltip to v7.0.16 (#17651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-21 13:12:02 +02:00
Paul Bottein
dddee87de3 Use gradient based on min/max color temp for tile card feature (#17612) 2023-08-21 13:03:20 +02:00
Paul Bottein
e8bd77a84e Add water heater more info to gallery (#17621) 2023-08-21 13:02:36 +02:00
Steve Repsher
46a036ddbe Improve frontend error messages written to system log (#17616) 2023-08-21 13:01:42 +02:00
Steve Repsher
bf912f7bd3 Add missing super calls to disconnectedCallback (#17641) 2023-08-21 12:57:38 +02:00
Michael Arthur
196c15ff3e Add lawn mower entity state, icon and color (#17558)
* start of lawn mower entity in the frontend

* added colours for states

* remove schedule states as no longer needed

* change mowing to teal

* remove docking as not included in architecture discussion and was missed
2023-08-21 12:19:15 +02:00
dependabot[bot]
d0a6e727f2 Bump actions/setup-node from 3.7.0 to 3.8.1 (#17650)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-21 10:58:07 +02:00
renovate[bot]
09697148cf Update Yarn to v3.6.2 (#17647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-20 21:46:00 -04:00
Franck Nijhof
76093d898d Fix template selector usage in config flows (#17643) 2023-08-20 12:45:39 -04:00
renovate[bot]
00c69c0fc3 Lock file maintenance (#17634)
* Lock file maintenance

* Limit time period for lock file maintenance

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-08-20 02:11:09 +00:00
renovate[bot]
93dd119ce5 Update dependency @rollup/plugin-node-resolve to v15.2.0 (#17640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-20 02:10:29 +00:00
renovate[bot]
e4f3211e9f Update dependency prettier to v3.0.2 (#17623)
* Update dependency prettier to v3.0.2

* Reformat

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-08-19 20:41:39 +00:00
renovate[bot]
c6ecdc9d5d Lock file maintenance (#17627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-19 16:06:31 -04:00
renovate[bot]
6bdd2d234d Update dependency @codemirror/language to v6.9.0 (#17632)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-19 15:39:44 -04:00
renovate[bot]
9d169bcbeb Update dependency systemjs to v6.14.2 (#17629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-19 15:36:45 -04:00
renovate[bot]
5c06ec1084 Update dependency eslint-import-resolver-webpack to v0.13.6 (#17628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-19 15:35:05 -04:00
RoboMagus
38a317b7e7 Fix ha-tabs chevrons bug (#17620) 2023-08-18 13:01:13 +00:00
Bram Kragten
593b176ab8 Bump version to 20230802.1 2023-08-18 11:18:24 +02:00
karwosts
1a15c8da8c Fix default precision display in entity settings (#17491) 2023-08-18 11:15:18 +02:00
Bram Kragten
060e67397a Prevent voice settings to override entity registry settings dialog (#17485) 2023-08-18 11:14:42 +02:00
Bram Kragten
d6de29ca8a Change logic to determine if forecast is hourly (#17486) 2023-08-18 11:14:15 +02:00
Bram Kragten
220767b347 Use service translations in logbook (#17461) 2023-08-18 11:13:57 +02:00
karwosts
79e1fbe076 Fix device config dialog when disabled (#17464) 2023-08-18 11:13:38 +02:00
renovate[bot]
cd19894ab0 Update dependency @lit-labs/context to v0.4.0 (#17613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-18 11:11:25 +02:00
Tomasz
705b6aeb4b Cover position tile feature (#16110)
* wip

* css + transtations

* Update types.ts

rollback changes done by VS Code

* fix

* Inverted slider

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-08-18 08:40:41 +00:00
Paul Bottein
6e27fbe10f Add unit when formatting attribute for display (#17607) 2023-08-18 10:28:27 +02:00
renovate[bot]
bbb99a6eee Update typescript-eslint monorepo to v6.4.0 (#17611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 17:53:39 -04:00
renovate[bot]
8411efc1c3 Update dependency marked to v7.0.3 (#17539)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 16:09:16 +00:00
Bram Kragten
88721df637 Bump marked (#17609) 2023-08-17 15:56:31 +00:00
renovate[bot]
265faddfa9 Update dependency @lit-labs/virtualizer to v2.0.5 (#17499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 15:51:59 +00:00
PaoloTK
6584dc70b7 Added support for color temperature tile feature (#16515)
* Added support for color temperature tile feature

* Update src/panels/lovelace/tile-features/hui-light-color-temp-tile-feature.ts

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-08-17 17:45:42 +02:00
renovate[bot]
d6b4dbe6a2 Update dependency @lit-labs/motion to v1.0.4 (#17498)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 15:37:03 +00:00
ildar170975
d579f93aa7 Update developer-tools-state.js: widen "Set state" controls (#17500) 2023-08-17 15:30:18 +00:00
renovate[bot]
b91261a789 Update dependency @lrnwebcomponents/simple-tooltip to v7.0.15 (#17324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 17:27:22 +02:00
renovate[bot]
872128d9a8 Update dependency luxon to v3.4.0 (#17559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 17:26:30 +02:00
renovate[bot]
4972db4648 Update dependency lint-staged to v14 (#17604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-17 17:26:03 +02:00
Paul Bottein
821cd7fe05 Add operation modes to tile card (#17597) 2023-08-17 17:19:46 +02:00
Paul Bottein
8c24ffa710 Add water heater state colors to gallery (#17606) 2023-08-17 15:51:49 +02:00
karwosts
d50a130345 Add show_name to state label badge (#17603) 2023-08-17 08:56:51 +02:00
renovate[bot]
ee8997fbd2 Update dependency lint-staged to v13.3.0 (#17602) 2023-08-16 17:01:00 -04:00
Bram Kragten
613cf932b5 Delay showing connection message (#17595) 2023-08-16 12:56:44 +00:00
Paul Bottein
12b61aea2f Add hvac modes to tile card (#17592) 2023-08-16 14:56:24 +02:00
Bram Kragten
51d9271c83 Change wording and icon for default thread router (#17593) 2023-08-16 14:25:29 +02:00
Paul Bottein
bd5264308f Sync selected icon with selected value in new select component (#17573) 2023-08-16 12:47:35 +02:00
Bram Kragten
2c17d2fead Allow to set default router for thread network (#17584)
* Allow to set default router for thread network

* Update thread-config-panel.ts
2023-08-16 11:52:42 +02:00
Bram Kragten
9f0b9782a0 Round altitude from GPS result (#17591) 2023-08-16 11:49:53 +02:00
Steve Repsher
88ff4c2fa8 Fix synchronous loading for ES5 build (#17174) 2023-08-15 19:48:51 +02:00
Steve Repsher
cba246fc7f Fix source URLs in source maps (#17585) 2023-08-15 15:10:12 +00:00
Bram Kragten
7d80eb06b0 20230802.0 (#17457) 2023-08-02 14:12:28 +02:00
Bram Kragten
a181189a49 20230801.0 (#17450) 2023-08-01 11:16:30 +02:00
Franck Nijhof
626b51112f 20230725.0 (#17407) 2023-07-25 18:02:12 +02:00
313 changed files with 16909 additions and 7070 deletions

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -55,9 +55,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -73,9 +73,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -91,9 +91,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -58,12 +58,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -21,10 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v3.7.0
uses: actions/setup-node@v3.8.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.3
uses: actions/checkout@v3.6.0
- name: Upload Translations
run: |

File diff suppressed because one or more lines are too long

View File

@@ -8,4 +8,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.6.1.cjs
yarnPath: .yarn/releases/yarn-3.6.3.cjs

View File

@@ -8,7 +8,7 @@ module.exports.sourceMapURL = () => {
const ref = env.version().endsWith("dev")
? process.env.GITHUB_SHA || "dev"
: env.version();
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}`;
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`;
};
// Files from NPM Packages that should not be imported

View File

@@ -1,10 +1,14 @@
import fs from "fs/promises";
import gulp from "gulp";
import path from "path";
import mapStream from "map-stream";
import transform from "gulp-json-transform";
import { LokaliseApi } from "@lokalise/node-api";
import JSZip from "jszip";
const inDirFrontend = "translations/frontend";
const inDirBackend = "translations/backend";
const inDir = "translations";
const inDirFrontend = `${inDir}/frontend`;
const inDirBackend = `${inDir}/backend`;
const srcMeta = "src/translations/translationMetadata.json";
const encoding = "utf8";
@@ -68,8 +72,9 @@ gulp.task("convert-backend-translations", function () {
});
gulp.task("check-translations-html", function () {
// We exclude backend translations because they are not compliant with the HTML rule for now
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
return gulp
.src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`])
.pipe(checkHtml());
});
gulp.task("check-all-files-exist", async function () {
@@ -89,7 +94,83 @@ gulp.task("check-all-files-exist", async function () {
await Promise.allSettled(writings);
});
const lokaliseProjects = {
backend: "130246255a974bd3b5e8a1.51616605",
frontend: "3420425759f6d6d241f598.13594006",
};
gulp.task("fetch-lokalise", async function () {
let apiKey;
try {
apiKey =
process.env.LOKALISE_TOKEN ||
(await fs.readFile(".lokalise_token", { encoding }));
} catch {
throw new Error(
"An Administrator Lokalise API token is required to download the latest set of translations. Place your token in a new file `.lokalise_token` in the repo root directory."
);
}
const lokaliseApi = new LokaliseApi({ apiKey });
const mkdirPromise = Promise.all([
fs.mkdir(inDirFrontend, { recursive: true }),
fs.mkdir(inDirBackend, { recursive: true }),
]);
await Promise.all(
Object.entries(lokaliseProjects).map(([project, projectId]) =>
lokaliseApi
.files()
.download(projectId, {
format: "json",
original_filenames: false,
replace_breaks: false,
json_unescaped_slashes: true,
export_empty_as: "skip",
})
.then((download) => fetch(download.bundle_url))
.then((response) => {
if (response.status === 200 || response.status === 0) {
return response.arrayBuffer();
}
throw new Error(response.statusText);
})
.then(JSZip.loadAsync)
.then(async (contents) => {
await mkdirPromise;
return Promise.all(
Object.keys(contents.files).map(async (filename) => {
const file = contents.file(filename);
if (!file) {
// no file, probably a directory
return Promise.resolve();
}
return file
.async("nodebuffer")
.then((content) =>
fs.writeFile(
path.join(
inDir,
project,
filename.split("/").splice(-1)[0]
),
content,
{ flag: "w", encoding }
)
);
})
);
})
)
);
});
gulp.task(
"check-downloaded-translations",
gulp.series("check-translations-html", "check-all-files-exist")
"download-translations",
gulp.series(
"fetch-lokalise",
"convert-backend-translations",
"check-translations-html",
"check-all-files-exist"
)
);

View File

@@ -1,5 +1,6 @@
const webpack = require("webpack");
const { existsSync } = require("fs");
const path = require("path");
const webpack = require("webpack");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const log = require("fancy-log");
@@ -191,19 +192,26 @@ const createWebpackConfig = ({
// Since production source maps don't include sources, we need to point to them elsewhere
// For dependencies, just provide the path (no source in browser)
// Otherwise, point to the raw code on GitHub for browser to load
devtoolModuleFilenameTemplate:
!isTestBuild && isProdBuild
? (info) => {
const sourcePath = info.resourcePath.replace(/^\.\//, "");
if (
sourcePath.startsWith("node_modules") ||
sourcePath.startsWith("webpack")
) {
return `no-source/${sourcePath}`;
...Object.fromEntries(
["", "Fallback"].map((v) => [
`devtool${v}ModuleFilenameTemplate`,
!isTestBuild && isProdBuild
? (info) => {
if (
!path.isAbsolute(info.absoluteResourcePath) ||
!existsSync(info.resourcePath) ||
info.resourcePath.startsWith("./node_modules")
) {
// Source URLs are unknown for dependencies, so we use a relative URL with a
// non - existent top directory. This results in a clean source tree in browser
// dev tools, and they stay happy getting 404s with valid requests.
return `/unknown${path.resolve("/", info.resourcePath)}`;
}
return new URL(info.resourcePath, bundle.sourceMapURL()).href;
}
return `${bundle.sourceMapURL()}/${sourcePath}`;
}
: undefined,
: undefined,
])
),
},
experiments: {
outputModule: true,

View File

@@ -1,4 +1,3 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html, css, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
@@ -7,6 +6,7 @@ import "../../../src/components/ha-switch";
import { HomeAssistant } from "../../../src/types";
import "./demo-card";
import type { DemoCardConfig } from "./demo-card";
import "../ha-demo-options";
@customElement("demo-cards")
class DemoCards extends LitElement {
@@ -20,20 +20,14 @@ class DemoCards extends LitElement {
render() {
return html`
<app-toolbar>
<div class="filters">
<ha-formfield label="Show config">
<ha-switch
.checked=${this._showConfig}
@change=${this._showConfigToggled}
>
</ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
</ha-formfield>
</div>
</app-toolbar>
<ha-demo-options>
<ha-formfield label="Show config">
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
</ha-formfield>
</ha-demo-options>
<div id="container">
<div class="cards">
${this.configs.map(
@@ -69,12 +63,6 @@ class DemoCards extends LitElement {
demo-card {
margin: 16px 16px 32px;
}
app-toolbar {
background-color: var(--light-primary-color);
}
.filters {
margin-left: 60px;
}
ha-formfield {
margin-right: 16px;
}

View File

@@ -1,93 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content";
class DemoMoreInfo extends PolymerElement {
static get template() {
return html`
<style>
.root {
display: flex;
}
#card {
max-width: 400px;
width: 100vw;
}
ha-card {
width: 352px;
padding: 20px 24px;
}
state-card-content {
display: block;
margin-bottom: 16px;
}
pre {
width: 400px;
margin: 0 16px;
overflow: auto;
color: var(--primary-text-color);
}
@media only screen and (max-width: 800px) {
.root {
flex-direction: column;
}
pre {
margin: 16px 0;
}
}
</style>
<div class="root">
<div id="card">
<ha-card>
<state-card-content
state-obj="[[_stateObj]]"
hass="[[hass]]"
in-dialog
></state-card-content>
<more-info-content
hass="[[hass]]"
state-obj="[[_stateObj]]"
></more-info-content>
</ha-card>
</div>
<template is="dom-if" if="[[showConfig]]">
<pre>[[_jsonEntity(_stateObj)]]</pre>
</template>
</div>
`;
}
static get properties() {
return {
hass: Object,
entityId: String,
showConfig: Boolean,
_stateObj: {
type: Object,
computed: "_getState(entityId, hass.states)",
},
};
}
_getState(entityId, states) {
return states[entityId];
}
_jsonEntity(stateObj) {
// We are caching some things on stateObj
// (it sucks, we will remove in the future)
const tmp = {};
Object.keys(stateObj).forEach((key) => {
if (key[0] !== "_") {
tmp[key] = stateObj[key];
}
});
return JSON.stringify(tmp, null, 2);
}
}
customElements.define("demo-more-info", DemoMoreInfo);

View File

@@ -0,0 +1,93 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content";
import "../ha-demo-options";
import { HomeAssistant } from "../../../src/types";
@customElement("demo-more-info")
class DemoMoreInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() public entityId!: string;
@property() public showConfig!: boolean;
render() {
const state = this._getState(this.entityId, this.hass.states);
return html`
<div class="root">
<div id="card">
<ha-card>
<state-card-content
.stateObj=${state}
.hass=${this.hass}
in-dialog
></state-card-content>
<more-info-content
.hass=${this.hass}
.stateObj=${state}
></more-info-content>
</ha-card>
</div>
${this.showConfig ? html`<pre>${this._jsonEntity(state)}</pre>` : ""}
</div>
`;
}
private _getState(entityId, states) {
return states[entityId];
}
private _jsonEntity(stateObj) {
// We are caching some things on stateObj
// (it sucks, we will remove in the future)
const tmp = {};
Object.keys(stateObj).forEach((key) => {
if (key[0] !== "_") {
tmp[key] = stateObj[key];
}
});
return JSON.stringify(tmp, null, 2);
}
static styles = css`
.root {
display: flex;
}
#card {
max-width: 400px;
width: 100vw;
}
ha-card {
width: 352px;
padding: 20px 24px;
}
state-card-content {
display: block;
margin-bottom: 16px;
}
pre {
width: 400px;
margin: 0 16px;
overflow: auto;
color: var(--primary-text-color);
}
@media only screen and (max-width: 800px) {
.root {
flex-direction: column;
}
pre {
margin: 16px 0;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info": DemoMoreInfo;
}
}

View File

@@ -1,83 +0,0 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch";
import "./demo-more-info";
class DemoMoreInfos extends PolymerElement {
static get template() {
return html`
<style>
#container {
min-height: calc(100vh - 128px);
background: var(--primary-background-color);
}
.cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
demo-more-info {
margin: 16px 16px 32px;
}
app-toolbar {
background-color: var(--light-primary-color);
}
.filters {
margin-left: 60px;
}
ha-formfield {
margin-right: 16px;
}
</style>
<app-toolbar>
<div class="filters">
<ha-formfield label="Show entities">
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
</ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
</ha-formfield>
</div>
</app-toolbar>
<div id="container">
<div class="cards">
<template is="dom-repeat" items="[[entities]]">
<demo-more-info
entity-id="[[item]]"
show-config="[[_showConfig]]"
hass="[[hass]]"
></demo-more-info>
</template>
</div>
</div>
`;
}
static get properties() {
return {
entities: Array,
hass: Object,
_showConfig: {
type: Boolean,
value: false,
},
};
}
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
_darkThemeToggled(ev) {
applyThemesOnElement(this.$.container, { themes: {} }, "default", {
dark: ev.target.checked,
});
}
}
customElements.define("demo-more-infos", DemoMoreInfos);

View File

@@ -0,0 +1,87 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch";
import "./demo-more-info";
import "../ha-demo-options";
import { HomeAssistant } from "../../../src/types";
@customElement("demo-more-infos")
class DemoMoreInfos extends LitElement {
@property() public hass!: HomeAssistant;
@property() public entities!: [];
@property({ attribute: false }) _showConfig: boolean = false;
render() {
return html`
<ha-demo-options>
<ha-formfield label="Show config">
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
</ha-formfield>
</ha-demo-options>
<div id="container">
<div class="cards">
${this.entities.map(
(item) =>
html`<demo-more-info
.entityId=${item}
.showConfig=${this._showConfig}
.hass=${this.hass}
></demo-more-info>`
)}
</div>
</div>
`;
}
static styles = css`
#container {
min-height: calc(100vh - 128px);
background: var(--primary-background-color);
}
.cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
demo-more-info {
margin: 16px 16px 32px;
}
ha-formfield {
margin-right: 16px;
}
`;
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
_darkThemeToggled(ev) {
applyThemesOnElement(
this.shadowRoot!.querySelector("#container"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
theme: "default",
},
"default",
{
dark: ev.target.checked,
}
);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-infos": DemoMoreInfos;
}
}

View File

@@ -0,0 +1,47 @@
import "@material/mwc-drawer";
import "@material/mwc-top-app-bar-fixed";
import { html, css, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "../../src/components/ha-icon-button";
import "../../src/managers/notification-manager";
import { haStyle } from "../../src/resources/styles";
import "./components/page-description";
@customElement("ha-demo-options")
class HaDemoOptions extends LitElement {
render() {
return html`<slot></slot>`;
}
static styles = [
haStyle,
css`
:host {
display: block;
background-color: var(--light-primary-color);
margin-left: 60px
margin-right: 60px;
display: var(--layout-horizontal_-_display);
-ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
-webkit-flex-direction: var(
--layout-horizontal_-_-webkit-flex-direction
);
flex-direction: var(--layout-horizontal_-_flex-direction);
-ms-flex-align: var(--layout-center_-_-ms-flex-align);
-webkit-align-items: var(--layout-center_-_-webkit-align-items);
align-items: var(--layout-center_-_align-items);
position: relative;
height: 64px;
padding: 0 16px;
pointer-events: none;
font-size: 20px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-demo-options": HaDemoOptions;
}
}

View File

@@ -0,0 +1,3 @@
---
title: Control Number Buttons
---

View File

@@ -0,0 +1,100 @@
import { LitElement, TemplateResult, css, html } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-number-buttons";
import { repeat } from "lit/directives/repeat";
import { ifDefined } from "lit/directives/if-defined";
const buttons: {
id: string;
label: string;
min?: number;
max?: number;
step?: number;
class?: string;
}[] = [
{
id: "basic",
label: "Basic",
},
{
id: "min_max_step",
label: "With min/max and step",
min: 5,
max: 25,
step: 0.5,
},
{
id: "custom",
label: "Custom",
class: "custom",
},
];
@customElement("demo-components-ha-control-number-buttons")
export class DemoHarControlNumberButtons extends LitElement {
@state() value = 5;
private _valueChanged(ev) {
this.value = ev.detail.value;
}
protected render(): TemplateResult {
return html`
${repeat(buttons, (button) => {
const { id, label, ...config } = button;
return html`
<ha-card>
<div class="card-content">
<label id=${id}>${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-control-number-buttons
.value=${this.value}
.min=${config.min}
.max=${config.max}
.step=${config.step}
class=${ifDefined(config.class)}
@value-changed=${this._valueChanged}
.label=${label}
>
</ha-control-number-buttons>
</div>
</ha-card>
`;
})}
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
pre {
margin-top: 0;
margin-bottom: 8px;
}
p {
margin: 0;
}
label {
font-weight: 600;
}
.custom {
color: #2196f3;
--control-number-buttons-color: #2196f3;
--control-number-buttons-background-color: #2196f3;
--control-number-buttons-background-opacity: 0.1;
--control-number-buttons-thickness: 100px;
--control-number-buttons-border-radius: 24px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-control-number-buttons": DemoHarControlNumberButtons;
}
}

View File

@@ -0,0 +1,3 @@
---
title: Control Select Menu
---

View File

@@ -0,0 +1,146 @@
import { mdiFan, mdiFanSpeed1, mdiFanSpeed2, mdiFanSpeed3 } from "@mdi/js";
import { LitElement, TemplateResult, css, html, nothing } from "lit";
import { customElement } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-select-menu";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-svg-icon";
type SelectMenuOptions = {
label: string;
value: string;
icon?: string;
};
type SelectMenu = {
label: string;
icon: string;
class?: string;
disabled?: boolean;
options: SelectMenuOptions[];
};
const selects: SelectMenu[] = [
{
label: "Basic select",
icon: mdiFan,
options: [
{
value: "low",
label: "Low",
},
{
value: "medium",
label: "Medium",
},
{
value: "high",
label: "High",
},
],
},
{
label: "Select with icons",
icon: mdiFan,
options: [
{
value: "low",
label: "Low",
icon: mdiFanSpeed1,
},
{
value: "medium",
label: "Medium",
icon: mdiFanSpeed2,
},
{
value: "high",
label: "High",
icon: mdiFanSpeed3,
},
],
},
{
label: "Disabled select",
icon: mdiFan,
options: [],
disabled: true,
},
];
@customElement("demo-components-ha-control-select-menu")
export class DemoHaControlSelectMenu extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card>
${repeat(
selects,
(select) => html`
<div class="card-content">
<ha-control-select-menu
.label=${select.label}
?disabled=${select.disabled}
fixedMenuPosition
naturalMenuWidth
>
<ha-svg-icon slot="icon" .path=${select.icon}></ha-svg-icon>
${select.options.map(
(option) => html`
<ha-list-item
.value=${option.value}
.graphic=${option.icon ? "icon" : undefined}
>
${option.icon
? html`
<ha-svg-icon
slot="graphic"
.path=${option.icon}
></ha-svg-icon>
`
: nothing}
${option.label ?? option.value}
</ha-list-item>
`
)}
</ha-control-select-menu>
</div>
`
)}
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
pre {
margin-top: 0;
margin-bottom: 8px;
}
p {
margin: 0;
}
label {
font-weight: 600;
}
.custom {
--control-button-icon-color: var(--primary-color);
--control-button-background-color: var(--primary-color);
--control-button-background-opacity: 0.2;
--control-button-border-radius: 18px;
height: 100px;
width: 100px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-control-select-menu": DemoHaControlSelectMenu;
}
}

View File

@@ -1,3 +0,0 @@
---
title: Temp Color Picker
---

View File

@@ -1,117 +0,0 @@
import "../../../../src/components/ha-temp-color-picker";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-slider";
@customElement("demo-components-ha-temp-color-picker")
export class DemoHaTempColorPicker extends LitElement {
@state()
min = 3000;
@state()
max = 7000;
@state()
value = 4000;
@state()
liveValue?: number;
private _minChanged(ev) {
this.min = Number(ev.target.value);
}
private _maxChanged(ev) {
this.max = Number(ev.target.value);
}
private _valueChanged(ev) {
this.value = Number(ev.target.value);
}
private _tempColorCursor(ev) {
this.liveValue = ev.detail.value;
}
private _tempColorChanged(ev) {
this.value = ev.detail.value;
}
protected render(): TemplateResult {
return html`
<ha-card>
<div class="card-content">
<p class="value">${this.liveValue ?? this.value} K</p>
<ha-temp-color-picker
.min=${this.min}
.max=${this.max}
.value=${this.value}
@value-changed=${this._tempColorChanged}
@cursor-moved=${this._tempColorCursor}
></ha-temp-color-picker>
<p>Min temp : ${this.min} K</p>
<ha-slider
step="1"
pin
min="2000"
max="10000"
.value=${this.min}
@change=${this._minChanged}
>
</ha-slider>
<p>Max temp : ${this.max} K</p>
<ha-slider
step="1"
pin
min="2000"
max="10000"
.value=${this.max}
@change=${this._maxChanged}
>
</ha-slider>
<p>Value : ${this.value} K</p>
<ha-slider
step="1"
pin
min=${this.min}
max=${this.max}
.value=${this.value}
@change=${this._valueChanged}
>
</ha-slider>
</div>
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.card-content {
display: flex;
align-items: center;
flex-direction: column;
}
ha-temp-color-picker {
width: 400px;
}
.value {
font-size: 22px;
font-weight: bold;
margin: 0 0 12px 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-temp-color-picker": DemoHaTempColorPicker;
}
}

View File

@@ -20,7 +20,7 @@ We want to make it as easy for designers to contribute as it is for developers.
- Meet us at <a href="https://discord.gg/BPBc8rZ9" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
- Find the lates UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
## Developers

View File

@@ -14,7 +14,7 @@ const ENTITIES = [
}),
getEntity("light", "bed_light", "on", {
friendly_name: "Bed Light",
supported_color_modes: [LightColorMode.HS],
supported_color_modes: [LightColorMode.HS, LightColorMode.COLOR_TEMP],
}),
getEntity("light", "unavailable", "unavailable", {
friendly_name: "Unavailable entity",
@@ -116,6 +116,15 @@ const CONFIGS = [
- type: "light-brightness"
`,
},
{
heading: "Light color temperature feature",
config: `
- type: tile
entity: light.bed_light
features:
- type: "color-temp"
`,
},
{
heading: "Vacuum commands feature",
config: `

View File

@@ -284,6 +284,13 @@ const ENTITIES: HassEntity[] = [
installed_version: "1.0.0",
latest_version: "2.0.0",
}),
createEntity("water_heater.off", "off"),
createEntity("water_heater.eco", "eco"),
createEntity("water_heater.electric", "electric"),
createEntity("water_heater.performance", "performance"),
createEntity("water_heater.high_demand", "high_demand"),
createEntity("water_heater.heat_pump", "heat_pump"),
createEntity("water_heater.gas", "gas"),
];
function createEntity(

View File

@@ -43,6 +43,28 @@ const ENTITIES = [
target_temp_low: 20,
target_temp_high: 25,
}),
getEntity("climate", "advanced", "auto", {
friendly_name: "Advanced hvac",
supported_features:
// eslint-disable-next-line no-bitwise
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE |
ClimateEntityFeature.TARGET_HUMIDITY |
ClimateEntityFeature.PRESET_MODE,
hvac_modes: ["auto", "off"],
hvac_mode: "auto",
preset_modes: ["eco", "comfort", "boost"],
preset_mode: "eco",
current_temperature: 18,
min_temp: 10,
max_temp: 30,
target_temp_step: 1,
target_temp_low: 20,
target_temp_high: 25,
current_humidity: 40,
min_humidity: 0,
max_humidity: 100,
humidity: 50,
}),
getEntity("climate", "unavailable", "unavailable", {
friendly_name: "Unavailable heater",
hvac_modes: ["heat", "off"],

View File

@@ -0,0 +1,3 @@
---
title: Water Heater
---

View File

@@ -0,0 +1,70 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import { WaterHeaterEntityFeature } from "../../../../src/data/water_heater";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import {
MockHomeAssistant,
provideHass,
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
const ENTITIES = [
getEntity("water_heater", "basic", "eco", {
friendly_name: "Basic heater",
operation_list: ["heat_pump", "eco", "performance", "off"],
operation_mode: "eco",
away_mode: "off",
target_temp_step: 1,
current_temperature: 55,
temperature: 60,
min_temp: 20,
max_temp: 70,
supported_features:
// eslint-disable-next-line no-bitwise
WaterHeaterEntityFeature.TARGET_TEMPERATURE |
WaterHeaterEntityFeature.OPERATION_MODE |
WaterHeaterEntityFeature.AWAY_MODE,
}),
getEntity("water_heater", "unavailable", "unavailable", {
friendly_name: "Unavailable heater",
operation_list: ["heat_pump", "eco", "performance", "off"],
operation_mode: "off",
min_temp: 20,
max_temp: 70,
supported_features:
// eslint-disable-next-line no-bitwise
WaterHeaterEntityFeature.TARGET_TEMPERATURE |
WaterHeaterEntityFeature.OPERATION_MODE,
}),
];
@customElement("demo-more-info-water-heater")
class DemoMoreInfoWaterHeater extends LitElement {
@property() public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-water-heater": DemoMoreInfoWaterHeater;
}
}

View File

@@ -31,8 +31,8 @@ export class HassioUploadBackup extends LitElement {
.icon=${mdiFolderUpload}
accept="application/x-tar"
label="Upload backup"
supports="Supports .TAR files"
@file-picked=${this._uploadFile}
auto-open-file-dialog
></ha-file-upload>
`;
}

View File

@@ -173,6 +173,7 @@ class HassioBackupDialog
private async _restoreClicked() {
const backupDetails = this._backupContent.backupDetails();
this._restoringBackup = true;
this._dialogParams?.onRestoring?.();
if (this._backupContent.backupType === "full") {
await this._fullRestoreClicked(backupDetails);
} else {
@@ -219,7 +220,7 @@ class HassioBackupDialog
this._error = error.body.message;
}
} else {
fireEvent(this, "restoring");
this._dialogParams?.onRestoring?.();
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
method: "POST",
body: JSON.stringify(backupDetails),
@@ -268,7 +269,7 @@ class HassioBackupDialog
}
);
} else {
fireEvent(this, "restoring");
this._dialogParams?.onRestoring?.();
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
method: "POST",
body: JSON.stringify(backupDetails),

View File

@@ -5,6 +5,7 @@ import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupDialogParams {
slug: string;
onDelete?: () => void;
onRestoring?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
localize?: LocalizeFunc;

View File

@@ -25,15 +25,15 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.22.10",
"@babel/runtime": "7.22.11",
"@braintree/sanitize-url": "6.0.4",
"@codemirror/autocomplete": "6.9.0",
"@codemirror/commands": "6.2.4",
"@codemirror/language": "6.8.0",
"@codemirror/commands": "6.2.5",
"@codemirror/language": "6.9.0",
"@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.1",
"@codemirror/search": "6.5.2",
"@codemirror/state": "6.2.1",
"@codemirror/view": "6.16.0",
"@codemirror/view": "6.17.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.10.0",
"@formatjs/intl-displaynames": "6.5.0",
@@ -50,10 +50,10 @@
"@fullcalendar/luxon3": "6.1.8",
"@fullcalendar/timegrid": "6.1.8",
"@lezer/highlight": "1.1.6",
"@lit-labs/context": "0.3.3",
"@lit-labs/motion": "1.0.3",
"@lit-labs/virtualizer": "2.0.4",
"@lrnwebcomponents/simple-tooltip": "7.0.11",
"@lit-labs/context": "0.4.0",
"@lit-labs/motion": "1.0.4",
"@lit-labs/virtualizer": "2.0.7",
"@lrnwebcomponents/simple-tooltip": "7.0.16",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-button": "0.27.0",
@@ -79,10 +79,9 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.0-pre.15",
"@material/web": "=1.0.0-pre.16",
"@mdi/js": "7.2.96",
"@mdi/svg": "7.2.96",
"@polymer/app-layout": "3.1.0",
"@polymer/iron-flex-layout": "3.0.1",
"@polymer/iron-input": "3.0.1",
"@polymer/iron-resizable-behavior": "3.0.1",
@@ -94,8 +93,8 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.1.5",
"@vaadin/vaadin-themable-mixin": "24.1.5",
"@vaadin/combo-box": "24.1.6",
"@vaadin/vaadin-themable-mixin": "24.1.6",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -103,17 +102,17 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"chart.js": "3.3.2",
"chart.js": "4.3.3",
"comlink": "4.4.1",
"core-js": "3.32.0",
"cropperjs": "1.5.13",
"core-js": "3.32.1",
"cropperjs": "1.6.0",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"fuse.js": "6.6.2",
"google-timezones-json": "1.2.0",
"hls.js": "1.4.10",
"hls.js": "1.4.12",
"home-assistant-js-websocket": "8.2.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.0",
@@ -121,8 +120,8 @@
"leaflet": "1.9.4",
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.3.0",
"marked": "4.3.0",
"luxon": "3.4.2",
"marked": "7.0.5",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@@ -133,10 +132,12 @@
"roboto-fontface": "0.10.0",
"rrule": "2.7.2",
"sortablejs": "1.15.0",
"stacktrace-js": "2.0.2",
"superstruct": "1.0.3",
"tinykeys": "2.1.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "1.0.35",
"unfetch": "5.0.0",
"vis-data": "7.1.6",
"vis-network": "9.1.6",
@@ -152,12 +153,13 @@
"xss": "1.0.14"
},
"devDependencies": {
"@babel/core": "7.22.10",
"@babel/core": "7.22.11",
"@babel/plugin-proposal-decorators": "7.22.10",
"@babel/plugin-transform-runtime": "7.22.10",
"@babel/preset-env": "7.22.10",
"@babel/preset-typescript": "7.22.5",
"@babel/preset-env": "7.22.14",
"@babel/preset-typescript": "7.22.11",
"@koa/cors": "4.0.0",
"@lokalise/node-api": "11.0.1",
"@octokit/auth-oauth-device": "6.0.0",
"@octokit/plugin-retry": "6.0.0",
"@octokit/rest": "20.0.1",
@@ -165,7 +167,7 @@
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-commonjs": "25.0.4",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.1.0",
"@rollup/plugin-node-resolve": "15.2.1",
"@rollup/plugin-replace": "5.0.2",
"@types/babel__plugin-transform-runtime": "7.9.2",
"@types/chromecast-caf-receiver": "6.0.9",
@@ -174,39 +176,39 @@
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.0",
"@types/js-yaml": "4.0.5",
"@types/leaflet": "1.9.3",
"@types/leaflet-draw": "1.0.7",
"@types/luxon": "3.3.1",
"@types/marked": "4.3.1",
"@types/leaflet": "1.9.4",
"@types/leaflet-draw": "1.0.8",
"@types/luxon": "3.3.2",
"@types/mocha": "10.0.1",
"@types/qrcode": "1.5.1",
"@types/qrcode": "1.5.2",
"@types/serve-handler": "6.1.1",
"@types/sortablejs": "1.15.1",
"@types/sortablejs": "1.15.2",
"@types/tar": "6.1.5",
"@types/ua-parser-js": "0.7.37",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.3.0",
"@typescript-eslint/parser": "6.3.0",
"@typescript-eslint/eslint-plugin": "6.5.0",
"@typescript-eslint/parser": "6.5.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.7",
"del": "7.0.0",
"eslint": "8.47.0",
"chai": "4.3.8",
"del": "7.1.0",
"eslint": "8.48.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "9.0.0",
"eslint-import-resolver-webpack": "0.13.4",
"eslint-import-resolver-webpack": "0.13.7",
"eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.28.0",
"eslint-plugin-lit": "1.8.3",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-lit": "1.9.1",
"eslint-plugin-lit-a11y": "3.0.0",
"eslint-plugin-unused-imports": "3.0.0",
"eslint-plugin-wc": "1.5.0",
"esprima": "4.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.1.1",
"glob": "10.3.3",
"glob": "10.3.4",
"gulp": "4.0.2",
"gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8",
@@ -217,16 +219,16 @@
"husky": "8.0.3",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "13.2.3",
"lint-staged": "14.0.1",
"lit-analyzer": "2.0.0-pre.3",
"lodash.template": "4.5.0",
"magic-string": "0.30.2",
"magic-string": "0.30.3",
"map-stream": "0.0.7",
"mocha": "10.2.0",
"object-hash": "3.0.0",
"open": "9.1.0",
"pinst": "3.0.0",
"prettier": "3.0.1",
"prettier": "3.0.3",
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
@@ -234,11 +236,11 @@
"serve-handler": "6.1.5",
"sinon": "15.2.0",
"source-map-url": "0.4.1",
"systemjs": "6.14.1",
"systemjs": "6.14.2",
"tar": "6.1.15",
"terser-webpack-plugin": "5.3.9",
"ts-lit-plugin": "2.0.0-pre.1",
"typescript": "5.1.6",
"typescript": "5.2.2",
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0",
"webpack": "5.88.2",
@@ -255,5 +257,5 @@
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
},
"packageManager": "yarn@3.6.1"
"packageManager": "yarn@3.6.3"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20230802.0"
version = "20230906.1"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@@ -15,7 +15,7 @@
"lockFileMaintenance": {
"description": ["Run after patch releases but before next beta"],
"enabled": true,
"schedule": ["on the 19th day of the month"]
"schedule": ["on the 19th day of the month before 4am"]
},
"packageRules": [
{

View File

@@ -8,40 +8,4 @@ set -eu -o pipefail
cd "$(dirname "$0")/.."
if [ -z "${LOKALISE_TOKEN-}" ] && [ ! -f .lokalise_token ] ; then
echo "Lokalise API token is required to download the latest set of" \
"translations. Please create an account by using the following link:" \
"https://lokalise.co/signup/3420425759f6d6d241f598.13594006/all/" \
"Place your token in a new file \".lokalise_token\" in the repo" \
"root directory."
exit 1
fi
# Load token from file if not already in the environment
[ -z "${LOKALISE_TOKEN-}" ] && LOKALISE_TOKEN="$(<.lokalise_token)"
declare -A PROJECT_ID=( \
[frontend]="3420425759f6d6d241f598.13594006" \
[backend]="130246255a974bd3b5e8a1.51616605" \
)
for project in ${!PROJECT_ID[*]}; do
LOCAL_DIR=`pwd`/translations/${project}
rm -f ${LOCAL_DIR}/* || mkdir -p ${LOCAL_DIR}
docker run \
-v ${LOCAL_DIR}:/opt/dest/locale \
--rm \
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d \
lokalise2 \
--token ${LOKALISE_TOKEN} \
--project-id ${PROJECT_ID[${project}]} \
file download \
--export-empty-as skip \
--format json \
--json-unescaped-slashes=true \
--replace-breaks=false \
--original-filenames=false \
--unzip-to /opt/dest
done
./node_modules/.bin/gulp check-downloaded-translations
./node_modules/.bin/gulp download-translations

View File

@@ -1,3 +1,4 @@
import punycode from "punycode";
import {
css,
CSSResultGroup,
@@ -7,7 +8,6 @@ import {
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import punycode from "punycode";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { extractSearchParamsObject } from "../common/url/search-params";
import "../components/ha-alert";
@@ -35,6 +35,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public oauth2State?: string;
@property() public translationFragment = "page-authorize";
@state() private _authProvider?: AuthProvider;
@state() private _authProviders?: AuthProvider[];
@@ -45,7 +47,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
constructor() {
super();
this.translationFragment = "page-authorize";
const query = extractSearchParamsObject() as AuthUrlSearchParams;
if (query.client_id) {
this.clientId = query.client_id;
@@ -102,7 +103,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
: nothing}
<ha-auth-flow
.resources=${this.resources}
.clientId=${this.clientId}
.redirectUri=${this.redirectUri}
.oauth2State=${this.oauth2State}

View File

@@ -107,6 +107,7 @@ export class HaPasswordManagerPolyfill extends LitElement {
.value=${this.stepData[schema.name] || ""}
.autocomplete=${schema.autocomplete}
@input=${this._valueChanged}
@change=${this._valueChanged}
/>
`;
}

View File

@@ -1,7 +1,7 @@
import { clamp } from "../number/clamp";
const DEFAULT_MIN_KELVIN = 2700;
const DEFAULT_MAX_KELVIN = 6500;
export const DEFAULT_MIN_KELVIN = 2700;
export const DEFAULT_MAX_KELVIN = 6500;
export const temperature2rgb = (
temperature: number

View File

@@ -49,6 +49,7 @@ import {
mdiProgressClock,
mdiRayVertex,
mdiRemote,
mdiRobotMower,
mdiRobotVacuum,
mdiScriptText,
mdiSineWave,
@@ -99,6 +100,7 @@ export const FIXED_DOMAIN_ICONS = {
input_number: mdiRayVertex,
input_select: mdiFormatListBulleted,
input_text: mdiFormTextbox,
lawn_mower: mdiRobotMower,
light: mdiLightbulb,
mailbox: mdiMailbox,
notify: mdiCommentAlert,
@@ -187,6 +189,7 @@ export const DOMAINS_WITH_CARD = [
"input_number",
"input_text",
"humidifier",
"lawn_mower",
"lock",
"media_player",
"number",

View File

@@ -1,6 +1,11 @@
import { HassConfig, HassEntity } from "home-assistant-js-websocket";
import {
DOMAIN_ATTRIBUTES_UNITS,
TEMPERATURE_ATTRIBUTES,
} from "../../data/entity_attributes";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { FrontendLocaleData } from "../../data/translation";
import { WeatherEntity, getWeatherUnit } from "../../data/weather";
import { HomeAssistant } from "../../types";
import checkValidDate from "../datetime/check_valid_date";
import { formatDate } from "../datetime/format_date";
@@ -9,8 +14,10 @@ import { formatNumber } from "../number/format_number";
import { capitalizeFirstLetter } from "../string/capitalize-first-letter";
import { isDate } from "../string/is_date";
import { isTimestamp } from "../string/is_timestamp";
import { blankBeforePercent } from "../translations/blank_before_percent";
import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain";
import { computeStateDomain } from "./compute_state_domain";
export const computeAttributeValueDisplay = (
localize: LocalizeFunc,
@@ -31,7 +38,40 @@ export const computeAttributeValueDisplay = (
// Number value, return formatted number
if (typeof attributeValue === "number") {
return formatNumber(attributeValue, locale);
const formattedValue = formatNumber(attributeValue, locale);
const domain = computeStateDomain(stateObj);
let unit = DOMAIN_ATTRIBUTES_UNITS[domain]?.[attribute] as
| string
| undefined;
if (domain === "light" && attribute === "brightness") {
const percentage = Math.round((attributeValue / 255) * 100);
return `${percentage}${blankBeforePercent(locale)}%`;
}
if (domain === "weather") {
unit = getWeatherUnit(config, stateObj as WeatherEntity, attribute);
}
if (unit === "%") {
return `${formattedValue}${blankBeforePercent(locale)}${unit}`;
}
if (unit === "°") {
return `${formattedValue}${unit}`;
}
if (unit) {
return `${formattedValue} ${unit}`;
}
if (TEMPERATURE_ATTRIBUTES.has(attribute)) {
return `${formattedValue} ${config.unit_system.temperature}`;
}
return formattedValue;
}
// Special handling in case this is a string with an known format

View File

@@ -26,6 +26,7 @@ export const FIXED_DOMAIN_STATES = {
humidifier: ["on", "off"],
input_boolean: ["on", "off"],
input_button: [],
lawn_mower: ["error", "paused", "mowing", "docked"],
light: ["on", "off"],
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
media_player: [

View File

@@ -34,6 +34,8 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
case "device_tracker":
case "person":
return compareState !== "not_home";
case "lawn_mower":
return ["mowing", "error"].includes(compareState);
case "lock":
return compareState !== "locked";
case "media_player":

View File

@@ -22,6 +22,7 @@ const STATE_COLORED_DOMAIN = new Set([
"group",
"humidifier",
"input_boolean",
"lawn_mower",
"light",
"lock",
"media_player",

View File

@@ -4,7 +4,7 @@ export const clamp = (value: number, min: number, max: number) =>
// Variant that only applies the clamping to a border if the border is defined
export const conditionalClamp = (value: number, min?: number, max?: number) => {
let result: number;
result = min ? Math.max(value, min) : value;
result = max ? Math.min(result, max) : result;
result = min != null ? Math.max(value, min) : value;
result = max != null ? Math.min(result, max) : result;
return result;
};

View File

@@ -108,7 +108,7 @@ export const formatNumber = (
* @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined`
*/
export const getNumberFormatOptions = (
entityState: HassEntity,
entityState?: HassEntity,
entity?: EntityRegistryDisplayEntry
): Intl.NumberFormatOptions | undefined => {
const precision = entity?.display_precision;
@@ -119,8 +119,8 @@ export const getNumberFormatOptions = (
};
}
if (
Number.isInteger(Number(entityState.attributes?.step)) &&
Number.isInteger(Number(entityState.state))
Number.isInteger(Number(entityState?.attributes?.step)) &&
Number.isInteger(Number(entityState?.state))
) {
return { maximumFractionDigits: 0 };
}

View File

@@ -3,25 +3,30 @@ import type { FrontendLocaleData } from "../../data/translation";
import type { HomeAssistant } from "../../types";
import type { LocalizeFunc } from "./localize";
export type FormatEntityStateFunc = {
formatEntityState: (stateObj: HassEntity, state?: string) => string;
formatEntityAttributeValue: (
stateObj: HassEntity,
attribute: string,
value?: any
) => string;
formatEntityAttributeName: (
stateObj: HassEntity,
attribute: string
) => string;
};
export type FormatEntityStateFunc = (
stateObj: HassEntity,
state?: string
) => string;
export type FormatEntityAttributeValueFunc = (
stateObj: HassEntity,
attribute: string,
value?: any
) => string;
export type formatEntityAttributeNameFunc = (
stateObj: HassEntity,
attribute: string
) => string;
export const computeFormatFunctions = async (
localize: LocalizeFunc,
locale: FrontendLocaleData,
config: HassConfig,
entities: HomeAssistant["entities"]
): Promise<FormatEntityStateFunc> => {
): Promise<{
formatEntityState: FormatEntityStateFunc;
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
formatEntityAttributeName: formatEntityAttributeNameFunc;
}> => {
const { computeStateDisplay } = await import(
"../entity/compute_state_display"
);

View File

@@ -11,23 +11,18 @@ export type LocalizeKeys =
| `ui.card.alarm_control_panel.${string}`
| `ui.card.weather.attributes.${string}`
| `ui.card.weather.cardinal_direction.${string}`
| `ui.card.lawn_mower.actions.${string}`
| `ui.components.calendar.event.rrule.${string}`
| `ui.components.logbook.${string}`
| `ui.components.selectors.file.${string}`
| `ui.dialogs.entity_registry.editor.${string}`
| `ui.dialogs.more_info_control.lawn_mower.${string}`
| `ui.dialogs.more_info_control.vacuum.${string}`
| `ui.dialogs.quick-bar.commands.${string}`
| `ui.dialogs.unhealthy.reason.${string}`
| `ui.dialogs.unsupported.reason.${string}`
| `ui.panel.config.${string}.${"caption" | "description"}`
| `ui.panel.config.automation.${string}`
| `ui.panel.config.dashboard.${string}`
| `ui.panel.config.devices.${string}`
| `ui.panel.config.energy.${string}`
| `ui.panel.config.info.${string}`
| `ui.panel.config.lovelace.${string}`
| `ui.panel.config.network.${string}`
| `ui.panel.config.scene.${string}`
| `ui.panel.config.zha.${string}`
| `ui.panel.config.zwave_js.${string}`
| `ui.panel.lovelace.card.${string}`

View File

@@ -1,100 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { EventsMixin } from "../../mixins/events-mixin";
import "./ha-progress-button";
/*
* @appliesMixin EventsMixin
*/
class HaCallServiceButton extends EventsMixin(PolymerElement) {
static get template() {
return html`
<ha-progress-button
id="progress"
progress="[[progress]]"
disabled="[[disabled]]"
on-click="buttonTapped"
tabindex="0"
><slot></slot
></ha-progress-button>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
progress: {
type: Boolean,
value: false,
},
domain: {
type: String,
},
service: {
type: String,
},
serviceData: {
type: Object,
value: {},
},
confirmation: {
type: String,
},
disabled: {
type: Boolean,
},
};
}
callService() {
this.progress = true;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const el = this;
const eventData = {
domain: this.domain,
service: this.service,
serviceData: this.serviceData,
};
this.hass
.callService(this.domain, this.service, this.serviceData)
.then(
() => {
el.progress = false;
el.$.progress.actionSuccess();
eventData.success = true;
},
() => {
el.progress = false;
el.$.progress.actionError();
eventData.success = false;
}
)
.then(() => {
el.fire("hass-service-called", eventData);
});
}
buttonTapped() {
if (this.confirmation) {
showConfirmationDialog(this, {
text: this.confirmation,
confirm: () => this.callService(),
});
} else {
this.callService();
}
}
}
customElements.define("ha-call-service-button", HaCallServiceButton);

View File

@@ -0,0 +1,92 @@
import { LitElement, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import "./ha-progress-button";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-call-service-button")
class HaCallServiceButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public progress = false;
@property() public domain!: string;
@property() public service!: string;
@property({ type: Object }) public serviceData = {};
@property() public confirmation?;
public render(): TemplateResult {
return html`
<ha-progress-button
.progress=${this.progress}
.disabled=${this.disabled}
@click=${this._buttonTapped}
tabindex="0"
>
<slot></slot
></ha-progress-button>
`;
}
private async _callService() {
this.progress = true;
const eventData = {
domain: this.domain,
service: this.service,
serviceData: this.serviceData,
success: false,
};
const progressElement =
this.shadowRoot!.querySelector("ha-progress-button")!;
try {
await this.hass.callService(this.domain, this.service, this.serviceData);
this.progress = false;
progressElement.actionSuccess();
eventData.success = true;
} catch (e) {
this.progress = false;
progressElement.actionError();
eventData.success = false;
return;
} finally {
fireEvent(this, "hass-service-called", eventData);
}
}
private _buttonTapped() {
if (this.confirmation) {
showConfirmationDialog(this, {
text: this.confirmation,
confirm: () => this._callService(),
});
} else {
this._callService();
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-call-service-button": HaCallServiceButton;
}
}
declare global {
// for fire event
interface HASSDomEvents {
"hass-service-called": {
domain: string;
service: string;
serviceData: object;
success: boolean;
};
}
}

View File

@@ -15,13 +15,20 @@ import { HomeAssistant } from "../../types";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
interface Tooltip extends TooltipModel<any> {
export interface ChartResizeOptions {
aspectRatio?: number;
height?: number;
width?: number;
}
interface Tooltip
extends Omit<TooltipModel<any>, "tooltipPosition" | "hasValue" | "getProps"> {
top: string;
left: string;
}
@customElement("ha-chart-base")
export default class HaChartBase extends LitElement {
export class HaChartBase extends LitElement {
public chart?: Chart;
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -45,14 +52,6 @@ export default class HaChartBase extends LitElement {
@state() private _hiddenDatasets: Set<number> = new Set();
private _releaseCanvas() {
// release the canvas memory to prevent
// safari from running out of memory.
if (this.chart) {
this.chart.destroy();
}
}
public disconnectedCallback() {
this._releaseCanvas();
super.disconnectedCallback();
@@ -65,6 +64,36 @@ export default class HaChartBase extends LitElement {
}
}
public updateChart = (
mode:
| "resize"
| "reset"
| "none"
| "hide"
| "show"
| "default"
| "active"
| undefined
): void => {
this.chart?.update(mode);
};
public resize = (options?: ChartResizeOptions): void => {
if (options?.aspectRatio && !options.height) {
options.height = Math.round(
(options.width ?? this.clientWidth) / options.aspectRatio
);
} else if (options?.aspectRatio && !options.width) {
options.width = Math.round(
(options.height ?? this.clientHeight) * options.aspectRatio
);
}
this.chart?.resize(
options?.width ?? this.clientWidth,
options?.height ?? this.clientHeight
);
};
protected firstUpdated() {
this._setupChart();
this.data.datasets.forEach((dataset, index) => {
@@ -80,14 +109,11 @@ export default class HaChartBase extends LitElement {
if (!this.hasUpdated || !this.chart) {
return;
}
if (changedProps.has("plugins")) {
if (changedProps.has("plugins") || changedProps.has("chartType")) {
this.chart.destroy();
this._setupChart();
return;
}
if (changedProps.has("chartType")) {
this.chart.config.type = this.chartType;
}
if (changedProps.has("data")) {
if (this._hiddenDatasets.size) {
this.data.datasets.forEach((dataset, index) => {
@@ -131,55 +157,70 @@ export default class HaChartBase extends LitElement {
</div>`
: ""}
<div
class="chartContainer"
class="animationContainer"
style=${styleMap({
height: `${this.height ?? this._chartHeight}px`,
height: `${this.height || this._chartHeight || 0}px`,
overflow: this._chartHeight ? "initial" : "hidden",
"padding-left": `${computeRTL(this.hass) ? 0 : this.paddingYAxis}px`,
"padding-right": `${computeRTL(this.hass) ? this.paddingYAxis : 0}px`,
})}
>
<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
class="chartContainer"
style=${styleMap({
height: `${
this.height ?? this._chartHeight ?? this.clientWidth / 2
}px`,
"padding-left": `${
computeRTL(this.hass) ? 0 : this.paddingYAxis
}px`,
"padding-right": `${
computeRTL(this.hass) ? this.paddingYAxis : 0
}px`,
})}
>
<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>
</div>
`;
}
@@ -213,6 +254,7 @@ export default class HaChartBase extends LitElement {
private _createOptions() {
return {
maintainAspectRatio: false,
...this.options,
plugins: {
...this.options?.plugins,
@@ -233,10 +275,10 @@ export default class HaChartBase extends LitElement {
return [
...(this.plugins || []),
{
id: "afterRenderHook",
afterRender: (chart) => {
id: "resizeHook",
resize: (chart) => {
const change = chart.height - (this._chartHeight ?? 0);
if (!this._chartHeight || change > 0 || change < -12) {
if (!this._chartHeight || change > 12 || change < -12) {
// hysteresis to prevent infinite render loops
this._chartHeight = chart.height;
}
@@ -288,21 +330,13 @@ export default class HaChartBase extends LitElement {
};
}
public updateChart = (
mode:
| "resize"
| "reset"
| "none"
| "hide"
| "show"
| "normal"
| "active"
| undefined
): void => {
private _releaseCanvas() {
// release the canvas memory to prevent
// safari from running out of memory.
if (this.chart) {
this.chart.update(mode);
this.chart.destroy();
}
};
}
static get styles(): CSSResultGroup {
return css`
@@ -310,11 +344,14 @@ export default class HaChartBase extends LitElement {
display: block;
position: var(--chart-base-position, relative);
}
.chartContainer {
.animationContainer {
overflow: hidden;
height: 0;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
.chartContainer {
position: relative;
}
canvas {
max-height: var(--chart-max-height, 400px);
}

View File

@@ -1,23 +1,28 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators";
import { property, query, state } from "lit/decorators";
import { getGraphColorByIndex } from "../../common/color/colors";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
import {
formatNumber,
numberFormatToLocale,
getNumberFormatOptions,
} from "../../common/number/format_number";
import { LineChartEntity, LineChartState } from "../../data/history";
import { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import {
ChartResizeOptions,
HaChartBase,
MIN_TIME_BETWEEN_UPDATES,
} from "./ha-chart-base";
const safeParseFloat = (value) => {
const parsed = parseFloat(value);
return isFinite(parsed) ? parsed : null;
};
class StateHistoryChartLine extends LitElement {
export class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data: LineChartEntity[] = [];
@@ -46,6 +51,12 @@ class StateHistoryChartLine extends LitElement {
private _chartTime: Date = new Date();
@query("ha-chart-base") private _chart?: HaChartBase;
public resize = (options?: ChartResizeOptions): void => {
this._chart?.resize(options);
};
protected render() {
return html`
<ha-chart-base
@@ -125,7 +136,17 @@ class StateHistoryChartLine extends LitElement {
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
this.hass.locale
this.hass.locale,
this.data[context.datasetIndex]?.entity_id
? getNumberFormatOptions(
this.hass.states[
this.data[context.datasetIndex].entity_id
],
this.hass.entities[
this.data[context.datasetIndex].entity_id
]
)
: undefined
)} ${this.unit}`,
},
},

View File

@@ -1,6 +1,6 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
import { fireEvent } from "../../common/dom/fire_event";
@@ -8,7 +8,11 @@ import { numberFormatToLocale } from "../../common/number/format_number";
import { computeRTL } from "../../common/util/compute_rtl";
import { TimelineEntity } from "../../data/history";
import { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import {
ChartResizeOptions,
HaChartBase,
MIN_TIME_BETWEEN_UPDATES,
} from "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const";
import { computeTimelineColor } from "./timeline-chart/timeline-color";
@@ -46,6 +50,12 @@ export class StateHistoryChartTimeline extends LitElement {
private _chartTime: Date = new Date();
@query("ha-chart-base") private _chart?: HaChartBase;
public resize = (options?: ChartResizeOptions): void => {
this._chart?.resize(options);
};
protected render() {
return html`
<ha-chart-base

View File

@@ -6,7 +6,13 @@ import {
nothing,
PropertyValues,
} from "lit";
import { customElement, eventOptions, property, state } from "lit/decorators";
import {
customElement,
eventOptions,
property,
queryAll,
state,
} from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import {
@@ -18,6 +24,9 @@ import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import "./state-history-chart-line";
import "./state-history-chart-timeline";
import type { StateHistoryChartLine } from "./state-history-chart-line";
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
import { ChartResizeOptions } from "./ha-chart-base";
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
@@ -75,6 +84,16 @@ export class StateHistoryCharts extends LitElement {
// @ts-ignore
@restoreScroll(".container") private _savedScrollPos?: number;
@queryAll("state-history-chart-line")
private _charts?: StateHistoryChartLine[];
public resize = (options?: ChartResizeOptions): void => {
this._charts?.forEach(
(chart: StateHistoryChartLine | StateHistoryChartTimeline) =>
chart.resize(options)
);
};
protected render() {
if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info">

View File

@@ -12,7 +12,7 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
@@ -31,6 +31,7 @@ import {
} from "../../data/recorder";
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
import type { ChartResizeOptions, HaChartBase } from "./ha-chart-base";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean",
@@ -42,7 +43,7 @@ export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
};
@customElement("statistics-chart")
class StatisticsChart extends LitElement {
export class StatisticsChart extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public statisticsData?: Statistics;
@@ -75,8 +76,14 @@ class StatisticsChart extends LitElement {
@state() private _chartOptions?: ChartOptions;
@query("ha-chart-base") private _chart?: HaChartBase;
private _computedStyle?: CSSStyleDeclaration;
public resize = (options?: ChartResizeOptions): void => {
this._chart?.resize(options);
};
protected shouldUpdate(changedProps: PropertyValues): boolean {
return changedProps.size > 1 || !changedProps.has("hass");
}

View File

@@ -1,3 +1,8 @@
import type {
BarControllerChartOptions,
BarControllerDatasetOptions,
} from "chart.js";
export interface TimeLineData {
start: Date;
end: Date;

View File

@@ -16,7 +16,7 @@ export interface TextBaroptions extends BarOptions {
export class TextBarElement extends BarElement {
static id = "textbar";
draw(ctx) {
draw(ctx: CanvasRenderingContext2D) {
super.draw(ctx);
const options = this.options as TextBaroptions;
const { x, y, base, width, text } = (

View File

@@ -2,6 +2,95 @@ import { BarController, BarElement } from "chart.js";
import { TimeLineData } from "./const";
import { TextBarProps } from "./textbar-element";
function borderProps(properties) {
let reverse;
let start;
let end;
let top;
let bottom;
if (properties.horizontal) {
reverse = properties.base > properties.x;
start = "left";
end = "right";
} else {
reverse = properties.base < properties.y;
start = "bottom";
end = "top";
}
if (reverse) {
top = "end";
bottom = "start";
} else {
top = "start";
bottom = "end";
}
return { start, end, reverse, top, bottom };
}
function setBorderSkipped(properties, options, stack, index) {
let edge = options.borderSkipped;
const res = {};
if (!edge) {
properties.borderSkipped = res;
return;
}
if (edge === true) {
properties.borderSkipped = {
top: true,
right: true,
bottom: true,
left: true,
};
return;
}
const { start, end, reverse, top, bottom } = borderProps(properties);
if (edge === "middle" && stack) {
properties.enableBorderRadius = true;
if ((stack._top || 0) === index) {
edge = top;
} else if ((stack._bottom || 0) === index) {
edge = bottom;
} else {
res[parseEdge(bottom, start, end, reverse)] = true;
edge = top;
}
}
res[parseEdge(edge, start, end, reverse)] = true;
properties.borderSkipped = res;
}
function parseEdge(edge, a, b, reverse) {
if (reverse) {
edge = swap(edge, a, b);
edge = startEnd(edge, b, a);
} else {
edge = startEnd(edge, a, b);
}
return edge;
}
function swap(orig, v1, v2) {
return orig === v1 ? v2 : orig === v2 ? v1 : orig;
}
function startEnd(v, start, end) {
return v === "start" ? start : v === "end" ? end : v;
}
function setInflateAmount(
properties,
{ inflateAmount }: { inflateAmount?: string | number },
ratio
) {
properties.inflateAmount =
inflateAmount === "auto" ? (ratio === 1 ? 0.33 : 0) : inflateAmount;
}
function parseValue(entry, item, vScale, i) {
const startValue = vScale.parse(entry.start, i);
const endValue = vScale.parse(entry.end, i);
@@ -97,7 +186,7 @@ export class TimelineController extends BarController {
bars: BarElement[],
start: number,
count: number,
mode: "reset" | "resize" | "none" | "hide" | "show" | "normal" | "active"
mode: "reset" | "resize" | "none" | "hide" | "show" | "default" | "active"
) {
const vScale = this._cachedMeta.vScale!;
const iScale = this._cachedMeta.iScale!;
@@ -114,15 +203,15 @@ export class TimelineController extends BarController {
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 parsed = this.getParsed(index);
const stack = (parsed._stacks || {})[vScale.axis];
const height = 10;
const properties: TextBarProps = {
@@ -145,7 +234,10 @@ export class TimelineController extends BarController {
backgroundColor: data.color,
};
}
const options = properties.options || bars[index].options;
setBorderSkipped(properties, options, stack, index);
setInflateAmount(properties, options, 1);
this.updateElement(bars[index], index, properties as any, mode);
}
}

View File

@@ -458,7 +458,8 @@ export class HaDataTable extends LitElement {
filteredData,
this._sortColumns[this._sortColumn],
this._sortDirection,
this._sortColumn
this._sortColumn,
this.hass.locale.language
)
: filteredData;

View File

@@ -1,6 +1,7 @@
// To use comlink under ES5
import "proxy-polyfill";
import { expose } from "comlink";
import "proxy-polyfill";
import { stringCompare } from "../../common/string/compare";
import type {
ClonedDataTableColumnData,
DataTableRowData,
@@ -39,7 +40,8 @@ const sortData = (
data: DataTableRowData[],
column: ClonedDataTableColumnData,
direction: SortingDirection,
sortColumn: string
sortColumn: string,
language?: string
) =>
data.sort((a, b) => {
let sort = 1;
@@ -58,13 +60,8 @@ const sortData = (
if (column.type === "numeric") {
valA = isNaN(valA) ? undefined : Number(valA);
valB = isNaN(valB) ? undefined : Number(valB);
} else {
if (typeof valA === "string") {
valA = valA.toUpperCase();
}
if (typeof valB === "string") {
valB = valB.toUpperCase();
}
} else if (typeof valA === "string" && typeof valB === "string") {
return sort * stringCompare(valA, valB, language);
}
// Ensure "undefined" and "null" are always sorted to the bottom

View File

@@ -27,10 +27,12 @@ export const filterData = (
filter: FilterDataParamTypes[2]
): Promise<ReturnType<FilterDataType>> =>
getWorker().filterData(data, columns, filter);
export const sortData = (
data: SortDataParamTypes[0],
columns: SortDataParamTypes[1],
direction: SortDataParamTypes[2],
sortColumn: SortDataParamTypes[3]
sortColumn: SortDataParamTypes[3],
language?: SortDataParamTypes[4]
): Promise<ReturnType<SortDataType>> =>
getWorker().sortData(data, columns, direction, sortColumn);
getWorker().sortData(data, columns, direction, sortColumn, language);

View File

@@ -324,6 +324,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
.renderer=${rowRenderer}
.disabled=${this.disabled}
.required=${this.required}
item-id-path="id"
item-value-path="id"
item-label-path="name"
@opened-changed=${this._openedChanged}

View File

@@ -1,11 +1,9 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, nothing } from "lit";
import { LitElement, PropertyValues, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { getStates } from "../../common/entity/get_states";
import { computeAttributeValueDisplay } from "../../common/entity/compute_attribute_display";
import { ValueChangedEvent, HomeAssistant } from "../../types";
import { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
@@ -58,20 +56,9 @@ class HaEntityStatePicker extends LitElement {
? getStates(state, this.attribute).map((key) => ({
value: key,
label: !this.attribute
? computeStateDisplay(
this.hass.localize,
? this.hass.formatEntityState(state, key)
: this.hass.formatEntityAttributeValue(
state,
this.hass.locale,
this.hass.config,
this.hass.entities,
key
)
: computeAttributeValueDisplay(
this.hass.localize,
state,
this.hass.locale,
this.hass.config,
this.hass.entities,
this.attribute,
key
),

View File

@@ -12,7 +12,6 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
import secondsToDuration from "../../common/datetime/seconds_to_duration";
import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
@@ -62,6 +61,8 @@ export class HaStateLabelBadge extends LitElement {
@property() public image?: string;
@property() public showName?: boolean;
@state() private _timerTimeRemaining?: number;
private _connected?: boolean;
@@ -132,7 +133,9 @@ export class HaStateLabelBadge extends LitElement {
entityState,
this._timerTimeRemaining
)}
.description=${this.name ?? computeStateName(entityState)}
.description=${this.showName === false
? undefined
: this.name ?? computeStateName(entityState)}
>
${!image && showIcon
? html`<ha-state-icon
@@ -188,13 +191,7 @@ export class HaStateLabelBadge extends LitElement {
this.hass!.locale,
getNumberFormatOptions(entityState, entry)
)
: computeStateDisplay(
this.hass!.localize,
entityState,
this.hass!.locale,
this.hass!.config,
this.hass!.entities
);
: this.hass!.formatEntityState(entityState);
}
}

View File

@@ -3,6 +3,7 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { HomeAssistant } from "../types";
import { formatNumber } from "../common/number/format_number";
let jsYamlPromise: Promise<typeof import("../resources/js-yaml-dump")>;
@@ -14,11 +15,19 @@ class HaAttributeValue extends LitElement {
@property() public attribute!: string;
@property({ type: Boolean, attribute: "hide-unit" })
public hideUnit?: boolean;
protected render() {
if (!this.stateObj) {
return nothing;
}
const attributeValue = this.stateObj.attributes[this.attribute];
if (typeof attributeValue === "number" && this.hideUnit) {
return formatNumber(attributeValue, this.hass.locale);
}
if (typeof attributeValue === "string") {
// URL handling
if (attributeValue.startsWith("http")) {

View File

@@ -1,12 +1,19 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-attribute-value";
import "./ha-expansion-panel";
@customElement("ha-attributes")
class HaAttributes extends LitElement {
@@ -18,16 +25,30 @@ class HaAttributes extends LitElement {
@state() private _expanded = false;
private get _filteredAttributes() {
return this.computeDisplayAttributes(
STATE_ATTRIBUTES.concat(
this.extraFilters ? this.extraFilters.split(",") : []
)
);
}
protected willUpdate(changedProperties: PropertyValues): void {
if (
changedProperties.has("extraFilters") ||
changedProperties.has("stateObj")
) {
this.toggleAttribute("empty", this._filteredAttributes.length === 0);
}
}
protected render() {
if (!this.stateObj) {
return nothing;
}
const attributes = this.computeDisplayAttributes(
STATE_ATTRIBUTES.concat(
this.extraFilters ? this.extraFilters.split(",") : []
)
);
const attributes = this._filteredAttributes;
if (attributes.length === 0) {
return nothing;
}

View File

@@ -1,11 +1,14 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/number/format_number";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate";
import { isUnavailableState } from "../data/entity";
import { CLIMATE_PRESET_NONE, ClimateEntity } from "../data/climate";
import { isUnavailableState, OFF } from "../data/entity";
import type { HomeAssistant } from "../types";
@customElement("ha-climate-state")
@@ -24,26 +27,24 @@ class HaClimateState extends LitElement {
${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`-
${computeAttributeValueDisplay(
this.hass.localize,
${this.hass.formatEntityAttributeValue(
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"preset_mode"
)}`
: ""}
: nothing}
</span>
<div class="unit">${this._computeTarget()}</div>`
: this._localizeState()}
</div>
${currentStatus && !isUnavailableState(this.stateObj.state)
? html`<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div>
</div>`
: ""}`;
? html`
<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div>
</div>
`
: nothing}`;
}
private _computeCurrentStatus(): string | undefined {
@@ -54,28 +55,28 @@ class HaClimateState extends LitElement {
this.stateObj.attributes.current_temperature != null &&
this.stateObj.attributes.current_humidity != null
) {
return `${formatNumber(
this.stateObj.attributes.current_temperature,
this.hass.locale
)} ${this.hass.config.unit_system.temperature}/
${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_temperature"
)}/
${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}`;
}
if (this.stateObj.attributes.current_temperature != null) {
return `${formatNumber(
this.stateObj.attributes.current_temperature,
this.hass.locale
)} ${this.hass.config.unit_system.temperature}`;
return this.hass.formatEntityAttributeValue(
this.stateObj,
"current_temperature"
);
}
if (this.stateObj.attributes.current_humidity != null) {
return `${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
return this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
);
}
return undefined;
@@ -90,39 +91,33 @@ class HaClimateState extends LitElement {
this.stateObj.attributes.target_temp_low != null &&
this.stateObj.attributes.target_temp_high != null
) {
return `${formatNumber(
this.stateObj.attributes.target_temp_low,
this.hass.locale
)}-${formatNumber(
this.stateObj.attributes.target_temp_high,
this.hass.locale
)} ${this.hass.config.unit_system.temperature}`;
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_temp_low"
)}-${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_temp_high"
)}`;
}
if (this.stateObj.attributes.temperature != null) {
return `${formatNumber(
this.stateObj.attributes.temperature,
this.hass.locale
)} ${this.hass.config.unit_system.temperature}`;
return this.hass.formatEntityAttributeValue(this.stateObj, "temperature");
}
if (
this.stateObj.attributes.target_humidity_low != null &&
this.stateObj.attributes.target_humidity_high != null
) {
return `${formatNumber(
this.stateObj.attributes.target_humidity_low,
this.hass.locale
)}-${formatNumber(
this.stateObj.attributes.target_humidity_high,
this.hass.locale
)} %`;
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_humidity_low"
)}-${this.hass.formatEntityAttributeValue(
this.stateObj,
"target_humidity_high"
)}`;
}
if (this.stateObj.attributes.humidity != null) {
return `${formatNumber(
this.stateObj.attributes.humidity,
this.hass.locale
)} %`;
return this.hass.formatEntityAttributeValue(this.stateObj, "humidity");
}
return "";
@@ -133,24 +128,17 @@ class HaClimateState extends LitElement {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
);
const stateString = this.hass.formatEntityState(this.stateObj);
return this.stateObj.attributes.hvac_action
? `${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"hvac_action"
)} (${stateString})`
: stateString;
if (this.stateObj.attributes.hvac_action && this.stateObj.state !== OFF) {
const actionString = this.hass.formatEntityAttributeValue(
this.stateObj,
"hvac_action"
);
return `${actionString} (${stateString})`;
}
return stateString;
}
static get styles(): CSSResultGroup {

View File

@@ -244,7 +244,6 @@ export class HaComboBox extends LitElement {
);
if (overlay) {
overlay.setAttribute("required-vertical-space", "0");
this._removeInert(overlay);
}
this._observeBody();
@@ -312,6 +311,10 @@ export class HaComboBox extends LitElement {
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
ev.stopPropagation();
if (!this.allowCustomValue) {
// @ts-ignore
this._comboBox._closeOnBlurIsPrevented = true;
}
const newValue = ev.detail.value;
if (newValue !== this.value) {
@@ -327,7 +330,7 @@ export class HaComboBox extends LitElement {
}
vaadin-combo-box-light {
position: relative;
--vaadin-combo-box-overlay-max-height: calc(45vh);
--vaadin-combo-box-overlay-max-height: calc(45vh - 56px);
}
ha-textfield {
width: 100%;

View File

@@ -59,6 +59,8 @@ const A11Y_KEY_CODES = new Set([
"End",
]);
export type ControlCircularSliderMode = "start" | "end" | "full";
@customElement("ha-control-circular-slider")
export class HaControlCircularSlider extends LitElement {
@property({ type: Boolean, reflect: true })
@@ -67,8 +69,11 @@ export class HaControlCircularSlider extends LitElement {
@property({ type: Boolean })
public dual?: boolean;
@property({ type: Boolean, reflect: true })
public inverted?: boolean;
@property({ type: String })
public mode?: ControlCircularSliderMode;
@property({ type: Boolean })
public inactive?: boolean;
@property({ type: String })
public label?: string;
@@ -407,12 +412,10 @@ export class HaControlCircularSlider extends LitElement {
protected renderArc(
id: string,
value: number | undefined,
inverted: boolean | undefined
mode: ControlCircularSliderMode
) {
if (this.disabled) return nothing;
const limit = inverted ? this.max : this.min;
const path = svgArc({
x: 0,
y: 0,
@@ -421,82 +424,100 @@ export class HaControlCircularSlider extends LitElement {
r: RADIUS,
});
const limit = mode === "end" ? this.max : this.min;
const current = this.current ?? limit;
const target = value ?? limit;
const showActive = inverted ? target <= current : current <= target;
const showActive =
mode === "end"
? target <= current
: mode === "start"
? current <= target
: false;
const activeArcDashArray = showActive
? inverted
const activeArc = showActive
? mode === "end"
? this._strokeDashArc(target, current)
: this._strokeDashArc(current, target)
: this._strokeCircleDashArc(target);
const arcDashArray = inverted
? this._strokeDashArc(target, limit)
: this._strokeDashArc(limit, target);
const coloredArc =
mode === "full"
? this._strokeDashArc(this.min, this.max)
: mode === "end"
? this._strokeDashArc(target, limit)
: this._strokeDashArc(limit, target);
const targetCircleDashArray = this._strokeCircleDashArc(target);
const targetCircle = this._strokeCircleDashArc(target);
const currentCircleDashArray =
const currentCircle =
this.current != null &&
showActive &&
current <= this.max &&
current >= this.min
this.current <= this.max &&
this.current >= this.min &&
(showActive || this.mode === "full")
? this._strokeCircleDashArc(this.current)
: undefined;
return svg`
<path
class="arc arc-clear"
d=${path}
stroke-dasharray=${arcDashArray[0]}
stroke-dashoffset=${arcDashArray[1]}
/>
<path
class="arc arc-background ${classMap({ [id]: true })}"
d=${path}
stroke-dasharray=${arcDashArray[0]}
stroke-dashoffset=${arcDashArray[1]}
/>
<path
.id=${id}
d=${path}
class="arc arc-active ${classMap({ [id]: true })}"
stroke-dasharray=${activeArcDashArray[0]}
stroke-dashoffset=${activeArcDashArray[1]}
role="slider"
tabindex="0"
aria-valuemin=${this.min}
aria-valuemax=${this.max}
aria-valuenow=${
this._localValue != null
? this._steppedValue(this._localValue)
: undefined
<g class=${classMap({ inactive: Boolean(this.inactive) })}>
<path
class="arc arc-clear"
d=${path}
stroke-dasharray=${coloredArc[0]}
stroke-dashoffset=${coloredArc[1]}
/>
<path
class="arc arc-colored ${classMap({ [id]: true })}"
d=${path}
stroke-dasharray=${coloredArc[0]}
stroke-dashoffset=${coloredArc[1]}
/>
<path
.id=${id}
d=${path}
class="arc arc-active ${classMap({ [id]: true })}"
stroke-dasharray=${activeArc[0]}
stroke-dashoffset=${activeArc[1]}
role="slider"
tabindex="0"
aria-valuemin=${this.min}
aria-valuemax=${this.max}
aria-valuenow=${
this._localValue != null
? this._steppedValue(this._localValue)
: undefined
}
aria-disabled=${this.disabled}
aria-label=${ifDefined(this.lowLabel ?? this.label)}
@keydown=${this._handleKeyDown}
@keyup=${this._handleKeyUp}
/>
${
currentCircle
? svg`
<path
class="current arc-current"
d=${path}
stroke-dasharray=${currentCircle[0]}
stroke-dashoffset=${currentCircle[1]}
/>
`
: nothing
}
aria-disabled=${this.disabled}
aria-label=${ifDefined(this.lowLabel ?? this.label)}
@keydown=${this._handleKeyDown}
@keyup=${this._handleKeyUp}
/>
${
currentCircleDashArray
? svg`
<path
class="current arc-current"
d=${path}
stroke-dasharray=${currentCircleDashArray[0]}
stroke-dashoffset=${currentCircleDashArray[1]}
/>
`
: nothing
}
<path
class="target"
d=${path}
stroke-dasharray=${targetCircleDashArray[0]}
stroke-dashoffset=${targetCircleDashArray[1]}
/>
<path
class="target-border ${classMap({ [id]: true })}"
d=${path}
stroke-dasharray=${targetCircle[0]}
stroke-dashoffset=${targetCircle[1]}
/>
<path
class="target"
d=${path}
stroke-dasharray=${targetCircle[0]}
stroke-dashoffset=${targetCircle[1]}
/>
</g>
`;
}
@@ -551,11 +572,11 @@ export class HaControlCircularSlider extends LitElement {
? this.renderArc(
this.dual ? "low" : "value",
lowValue,
this.inverted
(!this.dual && this.mode) || "start"
)
: nothing}
${this.dual && highValue != null
? this.renderArc("high", highValue, true)
? this.renderArc("high", highValue, "end")
: nothing}
</g>
</g>
@@ -634,6 +655,19 @@ export class HaControlCircularSlider extends LitElement {
opacity 180ms ease-in-out;
}
.target-border {
fill: none;
stroke-linecap: round;
stroke-width: 24px;
stroke: white;
transition:
stroke-width 300ms ease-in-out,
stroke-dasharray 300ms ease-in-out,
stroke-dashoffset 300ms ease-in-out,
stroke 180ms ease-in-out,
opacity 180ms ease-in-out;
}
.current {
fill: none;
stroke-linecap: round;
@@ -655,7 +689,7 @@ export class HaControlCircularSlider extends LitElement {
.arc-clear {
stroke: var(--clear-background-color);
}
.arc-background {
.arc-colored {
opacity: 0.5;
}
.arc-active {
@@ -667,6 +701,7 @@ export class HaControlCircularSlider extends LitElement {
.pressed .arc,
.pressed .target,
.pressed .target-border,
.pressed .current {
transition:
stroke-width 300ms ease-in-out,
@@ -674,6 +709,11 @@ export class HaControlCircularSlider extends LitElement {
opacity 180ms ease-in-out;
}
.inactive .arc,
.inactive .arc-current {
opacity: 0;
}
.value {
stroke: var(--control-circular-slider-color);
}

View File

@@ -0,0 +1,260 @@
import { mdiMinus, mdiPlus } from "@mdi/js";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { conditionalClamp } from "../common/number/clamp";
import { formatNumber } from "../common/number/format_number";
import { FrontendLocaleData } from "../data/translation";
import { fireEvent } from "../common/dom/fire_event";
const A11Y_KEY_CODES = new Set([
"ArrowRight",
"ArrowUp",
"ArrowLeft",
"ArrowDown",
"PageUp",
"PageDown",
"Home",
"End",
]);
@customElement("ha-control-number-buttons")
export class HaControlNumberButton extends LitElement {
@property({ attribute: false }) public locale?: FrontendLocaleData;
@property({ type: Boolean, reflect: true }) disabled = false;
@property() public label?: string;
@property({ type: Number }) public step?: number;
@property({ type: Number }) public value?: number;
@property({ type: Number }) public min?: number;
@property({ type: Number }) public max?: number;
@property({ attribute: "false" })
public formatOptions: Intl.NumberFormatOptions = {};
@query("#input") _input!: HTMLDivElement;
private boundedValue(value: number) {
const clamped = conditionalClamp(value, this.min, this.max);
return Math.round(clamped / this._step) * this._step;
}
private get _step() {
return this.step ?? 1;
}
private get _value() {
return this.value ?? 0;
}
private get _tenPercentStep() {
if (this.max == null || this.min == null) return this._step;
const range = this.max - this.min / 10;
if (range <= this._step) return this._step;
return Math.max(range / 10);
}
private _handlePlusButton() {
this._increment();
fireEvent(this, "value-changed", { value: this.value });
this._input.focus();
}
private _handleMinusButton() {
this._decrement();
fireEvent(this, "value-changed", { value: this.value });
this._input.focus();
}
private _increment() {
this.value = this.boundedValue(this._value + this._step);
}
private _decrement() {
this.value = this.boundedValue(this._value - this._step);
}
_handleKeyDown(e: KeyboardEvent) {
if (this.disabled) return;
if (!A11Y_KEY_CODES.has(e.code)) return;
e.preventDefault();
switch (e.code) {
case "ArrowRight":
case "ArrowUp":
this._increment();
break;
case "ArrowLeft":
case "ArrowDown":
this._decrement();
break;
case "PageUp":
this.value = this.boundedValue(this._value + this._tenPercentStep);
break;
case "PageDown":
this.value = this.boundedValue(this._value - this._tenPercentStep);
break;
case "Home":
if (this.min != null) {
this.value = this.min;
}
break;
case "End":
if (this.max != null) {
this.value = this.max;
}
break;
}
fireEvent(this, "value-changed", { value: this.value });
}
protected render(): TemplateResult {
const displayedValue =
this.value != null
? formatNumber(this.value, this.locale, this.formatOptions)
: "";
return html`
<div class="container">
<div
id="input"
class="value"
role="number-button"
.tabIndex=${this.disabled ? "-1" : "0"}
aria-valuenow=${this.value}
aria-valuemin=${this.min}
aria-valuemax=${this.max}
aria-label=${ifDefined(this.label)}
?disabled=${this.disabled}
@keydown=${this._handleKeyDown}
>
${displayedValue}
</div>
<button
class="button minus"
type="button"
tabindex="-1"
aria-label="decrement"
@click=${this._handleMinusButton}
.disabled=${this.disabled ||
(this.min != null && this._value <= this.min)}
>
<ha-svg-icon aria-hidden .path=${mdiMinus}></ha-svg-icon>
</button>
<button
class="button plus"
type="button"
tabindex="-1"
aria-label="increment"
@click=${this._handlePlusButton}
.disabled=${this.disabled ||
(this.max != null && this._value >= this.max)}
>
<ha-svg-icon aria-hidden .path=${mdiPlus}></ha-svg-icon>
</button>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
--control-number-buttons-focus-color: var(--primary-color);
--control-number-buttons-background-color: var(--disabled-color);
--control-number-buttons-background-opacity: 0.2;
--control-number-buttons-border-radius: 10px;
--mdc-icon-size: 16px;
height: 40px;
width: 200px;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
font-style: normal;
font-weight: 500;
transition: color 180ms ease-in-out;
}
:host([disabled]) {
color: var(--disabled-color);
}
.container {
position: relative;
width: 100%;
height: 100%;
}
.value {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
padding: 0 44px;
border-radius: var(--control-number-buttons-border-radius);
padding: 0;
margin: 0;
box-sizing: border-box;
line-height: 0;
overflow: hidden;
/* For safari border-radius overflow */
z-index: 0;
font-size: inherit;
color: inherit;
user-select: none;
-webkit-tap-highlight-color: transparent;
outline: none;
}
.value::before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: var(--control-number-buttons-background-color);
transition:
background-color 180ms ease-in-out,
opacity 180ms ease-in-out;
opacity: var(--control-number-buttons-background-opacity);
}
.value:focus-visible {
box-shadow: 0 0 0 2px var(--control-number-buttons-focus-color);
}
.button {
color: inherit;
position: absolute;
top: 0;
bottom: 0;
padding: 0;
width: 35px;
height: 40px;
border: none;
background: none;
cursor: pointer;
outline: none;
}
.button[disabled] {
opacity: 0.4;
pointer-events: none;
cursor: not-allowed;
}
.button.minus {
left: 0;
}
.button.plus {
right: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-control-number-buttons": HaControlNumberButton;
}
}

View File

@@ -13,6 +13,10 @@ import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
import "./ha-icon";
import type { HaIcon } from "./ha-icon";
import "./ha-svg-icon";
import type { HaSvgIcon } from "./ha-svg-icon";
@customElement("ha-control-select-menu")
export class HaControlSelectMenu extends SelectBase {
@@ -66,9 +70,7 @@ export class HaControlSelectMenu extends SelectBase {
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
>
<div class="icon">
<slot name="icon"></slot>
</div>
${this.renderIcon()}
<div class="content">
<p id="label" class="label">${this.label}</p>
${this.selectedText
@@ -84,6 +86,25 @@ export class HaControlSelectMenu extends SelectBase {
`;
}
private renderIcon() {
const index = this.mdcFoundation?.getSelectedIndex();
const items = this.menuElement?.items ?? [];
const item = index != null ? items[index] : undefined;
const icon =
item?.querySelector("[slot='graphic']") ??
(null as HaSvgIcon | HaIcon | null);
return html`
<div class="icon">
${icon && "path" in icon
? html`<ha-svg-icon .path=${icon.path}></ha-svg-icon>`
: icon && "icon" in icon
? html`<ha-icon .path=${icon.icon}></ha-icon>`
: html`<slot name="icon"></slot>`}
</div>
`;
}
protected onFocus() {
this.handleRippleFocus();
super.onFocus();
@@ -149,18 +170,15 @@ export class HaControlSelectMenu extends SelectBase {
--control-select-menu-text-color: var(--primary-text-color);
--control-select-menu-background-color: var(--disabled-color);
--control-select-menu-background-opacity: 0.2;
--control-select-menu-border-radius: 16px;
--control-select-menu-min-width: 120px;
--control-select-menu-max-width: 200px;
--control-select-menu-width: 100%;
--mdc-icon-size: 24px;
--control-select-menu-border-radius: 14px;
--mdc-icon-size: 20px;
width: auto;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
}
.select-anchor {
color: var(--control-select-menu-text-color);
height: 56px;
padding: 8px 12px;
height: 48px;
padding: 6px 10px;
overflow: hidden;
position: relative;
cursor: pointer;
@@ -177,12 +195,14 @@ export class HaControlSelectMenu extends SelectBase {
z-index: 0;
font-size: inherit;
transition: color 180ms ease-in-out;
color: var(--control-text-icon-color);
gap: 12px;
min-width: var(--control-select-menu-min-width);
max-width: var(--control-select-menu-max-width);
width: var(--control-select-menu-width);
gap: 10px;
width: 100%;
user-select: none;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.25px;
}
.content {
display: flex;
@@ -204,24 +224,14 @@ export class HaControlSelectMenu extends SelectBase {
.label {
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.4px;
}
.select-no-value .label {
font-size: 16px;
line-height: 24px;
letter-spacing: 0.5px;
}
.value {
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.5px;
font-size: inherit;
line-height: inherit;
letter-spacing: inherit;
}
.select-anchor::before {

View File

@@ -217,6 +217,7 @@ export class HaControlSelect extends LitElement {
transition: box-shadow 180ms ease-in-out;
font-style: normal;
font-weight: 500;
color: var(--primary-text-color);
user-select: none;
-webkit-tap-highlight-color: transparent;
}
@@ -267,7 +268,6 @@ export class HaControlSelect extends LitElement {
justify-content: center;
border-radius: var(--control-select-button-border-radius);
overflow: hidden;
color: var(--primary-text-color);
/* For safari border-radius overflow */
z-index: 0;
}
@@ -331,6 +331,7 @@ export class HaControlSelect extends LitElement {
:host([disabled]) {
--control-select-color: var(--disabled-color);
--control-select-focused-opacity: 0;
color: var(--disabled-color);
}
:host([disabled]) .option {
cursor: not-allowed;

View File

@@ -43,6 +43,9 @@ export class HaControlSlider extends LitElement {
@property({ type: Boolean, attribute: "show-handle" })
public showHandle = false;
@property({ type: Boolean, attribute: "inverted" })
public inverted = false;
@property({ type: Number })
public value?: number;
@@ -61,11 +64,16 @@ export class HaControlSlider extends LitElement {
public pressed = false;
valueToPercentage(value: number) {
return (this.boundedValue(value) - this.min) / (this.max - this.min);
const percentage =
(this.boundedValue(value) - this.min) / (this.max - this.min);
return this.inverted ? 1 - percentage : percentage;
}
percentageToValue(value: number) {
return (this.max - this.min) * value + this.min;
percentageToValue(percentage: number) {
return (
(this.max - this.min) * (this.inverted ? 1 - percentage : percentage) +
this.min
);
}
steppedValue(value: number) {

View File

@@ -155,11 +155,12 @@ export class HaConversationAgentPicker extends LitElement {
if (!this._configEntry) {
return;
}
showOptionsFlowDialog(
this,
this._configEntry,
await fetchIntegrationManifest(this.hass, this._configEntry.domain)
);
showOptionsFlowDialog(this, this._configEntry, {
manifest: await fetchIntegrationManifest(
this.hass,
this._configEntry.domain
),
});
}
static get styles(): CSSResultGroup {

View File

@@ -10,12 +10,12 @@ import "./ha-icon-button";
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
export const createCloseHeading = (
hass: HomeAssistant,
hass: HomeAssistant | undefined,
title: string | TemplateResult
) => html`
<div class="header_title">${title}</div>
<ha-icon-button
.label=${hass.localize("ui.dialogs.generic.close")}
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
.path=${mdiClose}
dialogAction="close"
class="header_button"

View File

@@ -1,16 +1,19 @@
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiDelete, mdiFileUpload } from "@mdi/js";
import { LitElement, PropertyValues, TemplateResult, css, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import "./ha-circular-progress";
import "./ha-button";
import "./ha-icon-button";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { ensureArray } from "../common/array/ensure-array";
import { bytesToString } from "../util/bytes-to-string";
declare global {
interface HASSDomEvents {
"file-picked": { files: FileList };
"file-picked": { files: File[] };
}
}
@@ -22,12 +25,22 @@ export class HaFileUpload extends LitElement {
@property() public icon?: string;
@property() public label!: string;
@property() public label?: string;
@property() public value: string | TemplateResult | null = null;
@property() public secondary?: string;
@property() public supports?: string;
@property() public value?: File | File[] | FileList | string;
@property({ type: Boolean }) private multiple = false;
@property({ type: Boolean, reflect: true }) public disabled: boolean = false;
@property({ type: Boolean }) private uploading = false;
@property({ type: Number }) private progress?: number;
@property({ type: Boolean, attribute: "auto-open-file-dialog" })
private autoOpenFileDialog = false;
@@ -45,72 +58,102 @@ export class HaFileUpload extends LitElement {
public render(): TemplateResult {
return html`
${this.uploading
? html`<ha-circular-progress
alt="Uploading"
size="large"
active
></ha-circular-progress>`
: html`
<label
for="input"
class="mdc-text-field mdc-text-field--filled ${classMap({
"mdc-text-field--focused": this._drag,
"mdc-text-field--with-leading-icon": Boolean(this.icon),
"mdc-text-field--with-trailing-icon": Boolean(this.value),
})}"
@drop=${this._handleDrop}
@dragenter=${this._handleDragStart}
@dragover=${this._handleDragStart}
@dragleave=${this._handleDragEnd}
@dragend=${this._handleDragEnd}
>
<span class="mdc-text-field__ripple"></span>
<span
class="mdc-floating-label ${this.value || this._drag
? "mdc-floating-label--float-above"
: ""}"
id="label"
>${this.label}</span
? html`<div class="container">
<div class="row">
<span class="header"
>${this.value
? this.hass?.localize(
"ui.components.file-upload.uploading_name",
{ name: this.value }
)
: this.hass?.localize(
"ui.components.file-upload.uploading"
)}</span
>
${this.icon
? html`<span
class="mdc-text-field__icon mdc-text-field__icon--leading"
>
<ha-icon-button
@click=${this._openFilePicker}
.path=${this.icon}
></ha-icon-button>
</span>`
${this.progress
? html`<span class="progress"
>${this.progress}${blankBeforePercent(
this.hass!.locale
)}%</span
>`
: ""}
<div class="value">${this.value}</div>
<input
id="input"
type="file"
class="mdc-text-field__input file"
accept=${this.accept}
@change=${this._handleFilePicked}
aria-labelledby="label"
/>
${this.value
? html`<span
class="mdc-text-field__icon mdc-text-field__icon--trailing"
</div>
<mwc-linear-progress
.indeterminate=${!this.progress}
.progress=${this.progress ? this.progress / 100 : undefined}
></mwc-linear-progress>
</div>`
: html`<label
for=${this.value ? "" : "input"}
class="container ${classMap({
dragged: this._drag,
multiple: this.multiple,
value: Boolean(this.value),
})}"
@drop=${this._handleDrop}
@dragenter=${this._handleDragStart}
@dragover=${this._handleDragStart}
@dragleave=${this._handleDragEnd}
@dragend=${this._handleDragEnd}
>${!this.value
? html`<ha-svg-icon
class="big-icon"
.path=${this.icon || mdiFileUpload}
></ha-svg-icon>
<ha-button unelevated @click=${this._openFilePicker}>
${this.label ||
this.hass?.localize("ui.components.file-upload.label")}
</ha-button>
<span class="secondary"
>${this.secondary ||
this.hass?.localize(
"ui.components.file-upload.secondary"
)}</span
>
<ha-icon-button
slot="suffix"
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.close") ||
"close"}
.path=${mdiClose}
></ha-icon-button>
</span>`
: ""}
<span
class="mdc-line-ripple ${this._drag
? "mdc-line-ripple--active"
: ""}"
></span>
</label>
`}
<span class="supports">${this.supports}</span>`
: typeof this.value === "string"
? html`<div class="row">
<div class="value" @click=${this._openFilePicker}>
<ha-svg-icon
.path=${this.icon || mdiFileUpload}
></ha-svg-icon>
${this.value}
</div>
<ha-icon-button
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.delete") ||
"Delete"}
.path=${mdiDelete}
></ha-icon-button>
</div>`
: (this.value instanceof FileList
? Array.from(this.value)
: ensureArray(this.value)
).map(
(file) =>
html`<div class="row">
<div class="value" @click=${this._openFilePicker}>
<ha-svg-icon
.path=${this.icon || mdiFileUpload}
></ha-svg-icon>
${file.name} - ${bytesToString(file.size)}
</div>
<ha-icon-button
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.delete") ||
"Delete"}
.path=${mdiDelete}
></ha-icon-button>
</div>`
)}
<input
id="input"
type="file"
class="file"
.accept=${this.accept}
.multiple=${this.multiple}
@change=${this._handleFilePicked}
/></label>`}
`;
}
@@ -122,7 +165,12 @@ export class HaFileUpload extends LitElement {
ev.preventDefault();
ev.stopPropagation();
if (ev.dataTransfer?.files) {
fireEvent(this, "file-picked", { files: ev.dataTransfer.files });
fireEvent(this, "file-picked", {
files:
this.multiple || ev.dataTransfer.files.length === 1
? Array.from(ev.dataTransfer.files)
: [ev.dataTransfer.files[0]],
});
}
this._drag = false;
}
@@ -140,92 +188,121 @@ export class HaFileUpload extends LitElement {
}
private _handleFilePicked(ev) {
if (ev.target.files.length === 0) {
return;
}
this.value = ev.target.files;
fireEvent(this, "file-picked", { files: ev.target.files });
}
private _clearValue(ev: Event) {
ev.preventDefault();
this.value = null;
this._input!.value = "";
this.value = undefined;
fireEvent(this, "change");
}
static get styles() {
return [
styles,
css`
:host {
display: block;
}
.mdc-text-field--filled {
height: auto;
padding-top: 16px;
cursor: pointer;
}
.mdc-text-field--filled.mdc-text-field--with-trailing-icon {
padding-top: 28px;
}
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
color: var(--secondary-text-color);
}
.mdc-text-field--filled.mdc-text-field--with-trailing-icon
.mdc-text-field__icon {
align-self: flex-end;
}
.mdc-text-field__icon--leading {
margin-bottom: 12px;
inset-inline-start: initial;
inset-inline-end: 0px;
direction: var(--direction);
}
.mdc-text-field--filled .mdc-floating-label--float-above {
transform: scale(0.75);
top: 8px;
}
.mdc-floating-label {
inset-inline-start: 16px !important;
inset-inline-end: initial !important;
direction: var(--direction);
}
.mdc-text-field--filled .mdc-floating-label {
inset-inline-start: 48px !important;
inset-inline-end: initial !important;
direction: var(--direction);
}
.mdc-text-field__icon--trailing {
pointer-events: auto !important;
}
.dragged:before {
position: var(--layout-fit_-_position);
top: var(--layout-fit_-_top);
right: var(--layout-fit_-_right);
bottom: var(--layout-fit_-_bottom);
left: var(--layout-fit_-_left);
background: currentColor;
content: "";
opacity: var(--dark-divider-opacity);
pointer-events: none;
border-radius: 4px;
}
.value {
width: 100%;
}
input.file {
display: none;
}
img {
max-width: 100%;
max-height: 125px;
}
ha-icon-button {
--mdc-icon-button-size: 24px;
--mdc-icon-size: 20px;
}
ha-circular-progress {
display: block;
text-align-last: center;
}
`,
];
return css`
:host {
display: block;
height: 240px;
}
:host([disabled]) {
pointer-events: none;
color: var(--disabled-text-color);
}
.container {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: solid 1px
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
border-radius: var(--mdc-shape-small, 4px);
height: 100%;
}
label.container {
border: dashed 1px
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
cursor: pointer;
}
:host([disabled]) .container {
border-color: var(--disabled-color);
}
label.dragged {
border-color: var(--primary-color);
}
.dragged:before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: var(--primary-color);
content: "";
opacity: var(--dark-divider-opacity);
pointer-events: none;
border-radius: var(--mdc-shape-small, 4px);
}
label.value {
cursor: default;
}
label.value.multiple {
justify-content: unset;
overflow: auto;
}
.highlight {
color: var(--primary-color);
}
.row {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
padding: 0 16px;
box-sizing: border-box;
}
ha-button {
margin-bottom: 4px;
}
.supports {
color: var(--secondary-text-color);
font-size: 12px;
}
:host([disabled]) .secondary {
color: var(--disabled-text-color);
}
input.file {
display: none;
}
.value {
cursor: pointer;
}
.value ha-svg-icon {
margin-right: 8px;
}
.big-icon {
--mdc-icon-size: 48px;
margin-bottom: 8px;
}
ha-button {
--mdc-button-outline-color: var(--primary-color);
--mdc-icon-button-size: 24px;
}
mwc-linear-progress {
width: 100%;
padding: 16px;
box-sizing: border-box;
}
.header {
font-weight: 500;
}
.progress {
color: var(--secondary-text-color);
}
`;
}
}

View File

@@ -27,7 +27,8 @@ export const computeInitialHaFormData = (
data[field.name] = 0.0;
} else if (field.type === "select") {
if (field.options.length) {
data[field.name] = field.options[0][0];
const val = field.options[0];
data[field.name] = Array.isArray(val) ? val[0] : val;
}
} else if (field.type === "positive_time_period_dict") {
data[field.name] = {
@@ -47,11 +48,12 @@ export const computeInitialHaFormData = (
} else if ("boolean" in selector) {
data[field.name] = false;
} else if (
"text" in selector ||
"addon" in selector ||
"attribute" in selector ||
"file" in selector ||
"icon" in selector ||
"template" in selector ||
"text" in selector ||
"theme" in selector
) {
data[field.name] = "";
@@ -59,7 +61,8 @@ export const computeInitialHaFormData = (
data[field.name] = selector.number?.min ?? 0;
} else if ("select" in selector) {
if (selector.select?.options.length) {
data[field.name] = selector.select.options[0][0];
const val = selector.select.options[0];
data[field.name] = typeof val === "string" ? val : val.value;
}
} else if ("duration" in selector) {
data[field.name] = {

View File

@@ -68,6 +68,7 @@ export class HaFormString extends LitElement implements HaFormElement {
: this.schema.description?.suffix}
.validationMessage=${this.schema.required ? "Required" : undefined}
@input=${this._valueChanged}
@change=${this._valueChanged}
></ha-textfield>
${isPassword
? html`<ha-icon-button

View File

@@ -7,6 +7,12 @@ import { hsv2rgb, rgb2hex } from "../common/color/convert-color";
import { rgbw2rgb, rgbww2rgb } from "../common/color/convert-light-color";
import { fireEvent } from "../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"cursor-moved": { value?: any };
}
}
function xy2polar(x: number, y: number) {
const r = Math.sqrt(x * x + y * y);
const phi = Math.atan2(y, x);

View File

@@ -1,9 +1,5 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/number/format_number";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { isUnavailableState, OFF } from "../data/entity";
import { HumidifierEntity } from "../data/humidifier";
import type { HomeAssistant } from "../types";
@@ -23,12 +19,8 @@ class HaHumidifierState extends LitElement {
${this._localizeState()}
${this.stateObj.attributes.mode
? html`-
${computeAttributeValueDisplay(
this.hass.localize,
${this.hass.formatEntityAttributeValue(
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"mode"
)}`
: ""}
@@ -51,10 +43,10 @@ class HaHumidifierState extends LitElement {
}
if (this.stateObj.attributes.current_humidity != null) {
return `${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"current_humidity"
)}`;
}
return undefined;
@@ -66,10 +58,10 @@ class HaHumidifierState extends LitElement {
}
if (this.stateObj.attributes.humidity != null) {
return `${formatNumber(
this.stateObj.attributes.humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
return `${this.hass.formatEntityAttributeValue(
this.stateObj,
"humidity"
)}`;
}
return "";
@@ -80,24 +72,17 @@ class HaHumidifierState extends LitElement {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
const stateString = computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
);
const stateString = this.hass.formatEntityState(this.stateObj);
return this.stateObj.attributes.action && this.stateObj.state !== OFF
? `${computeAttributeValueDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"action"
)} (${stateString})`
: stateString;
if (this.stateObj.attributes.action && this.stateObj.state !== OFF) {
const actionString = this.hass.formatEntityAttributeValue(
this.stateObj,
"action"
);
return `${actionString} (${stateString})`;
}
return stateString;
}
static get styles(): CSSResultGroup {

View File

@@ -14,17 +14,17 @@ export class HaIconButtonGroup extends LitElement {
display: flex;
flex-direction: row;
align-items: center;
height: 56px;
height: 48px;
border-radius: 28px;
background-color: rgba(139, 145, 151, 0.1);
box-sizing: border-box;
width: auto;
padding: 4px;
gap: 4px;
padding: 0;
}
::slotted(.separator) {
background-color: rgba(var(--rgb-primary-text-color), 0.15);
width: 1px;
margin: 0 1px;
height: 40px;
}
`;

View File

@@ -7,6 +7,7 @@ import { formatLanguageCode } from "../common/language/format_language";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { FrontendLocaleData } from "../data/translation";
import "../resources/intl-polyfill";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
@@ -20,7 +21,7 @@ export class HaLanguagePicker extends LitElement {
@property() public languages?: string[];
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@@ -41,7 +42,18 @@ export class HaLanguagePicker extends LitElement {
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("languages") || changedProperties.has("value")) {
const localeChanged =
changedProperties.has("hass") &&
this.hass &&
changedProperties.get("hass") &&
changedProperties.get("hass").locale.language !==
this.hass.locale.language;
if (
changedProperties.has("languages") ||
changedProperties.has("value") ||
localeChanged
) {
this._select.layoutOptions();
if (this._select.value !== this.value) {
fireEvent(this, "value-changed", { value: this._select.value });
@@ -51,24 +63,27 @@ export class HaLanguagePicker extends LitElement {
}
const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.hass.locale,
this.nativeName
this.nativeName,
this.hass?.locale
);
const selectedItem = languageOptions.find(
const selectedItemIndex = languageOptions.findIndex(
(option) => option.value === this.value
);
if (!selectedItem) {
if (selectedItemIndex === -1) {
this.value = undefined;
}
if (localeChanged) {
this._select.select(selectedItemIndex);
}
}
}
private _getLanguagesOptions = memoizeOne(
(languages: string[], locale: FrontendLocaleData, nativeName: boolean) => {
(languages: string[], nativeName: boolean, locale?: FrontendLocaleData) => {
let options: { label: string; value: string }[] = [];
if (nativeName) {
const translations = this.hass.translationMetadata.translations;
const translations = translationMetadata.translations;
options = languages.map((lang) => {
let label = translations[lang]?.nativeName;
if (!label) {
@@ -87,14 +102,14 @@ export class HaLanguagePicker extends LitElement {
label,
};
});
} else {
} else if (locale) {
options = languages.map((lang) => ({
value: lang,
label: formatLanguageCode(lang, locale),
}));
}
if (!this.noSort) {
if (!this.noSort && locale) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, locale.language)
);
@@ -104,20 +119,14 @@ export class HaLanguagePicker extends LitElement {
);
private _computeDefaultLanguageOptions() {
if (!this.hass.translationMetadata?.translations) {
return;
}
this._defaultLanguages = Object.keys(
this.hass.translationMetadata.translations
);
this._defaultLanguages = Object.keys(translationMetadata.translations);
}
protected render() {
const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.hass.locale,
this.nativeName
this.nativeName,
this.hass?.locale
);
const value =
@@ -125,9 +134,10 @@ export class HaLanguagePicker extends LitElement {
return html`
<ha-select
.label=${this.label ||
this.hass.localize("ui.components.language-picker.language")}
.value=${value}
.label=${this.label ??
(this.hass?.localize("ui.components.language-picker.language") ||
"Language")}
.value=${value || ""}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@@ -137,9 +147,9 @@ export class HaLanguagePicker extends LitElement {
>
${languageOptions.length === 0
? html`<ha-list-item value=""
>${this.hass.localize(
>${this.hass?.localize(
"ui.components.language-picker.no_languages"
)}</ha-list-item
) || "No languages"}</ha-list-item
>`
: languageOptions.map(
(option) => html`
@@ -162,7 +172,7 @@ export class HaLanguagePicker extends LitElement {
private _changed(ev): void {
const target = ev.target as HaSelect;
if (!this.hass || target.value === "" || target.value === this.value) {
if (target.value === "" || target.value === this.value) {
return;
}
this.value = target.value;

View File

@@ -0,0 +1,91 @@
import "@material/mwc-button";
import { CSSResultGroup, LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { supportsFeature } from "../common/entity/supports-feature";
import {
LawnMowerEntity,
LawnMowerEntityFeature,
LawnMowerEntityState,
} from "../data/lawn_mower";
import { HomeAssistant } from "../types";
type LawnMowerAction = {
action: string;
service: string;
feature: LawnMowerEntityFeature;
};
const LAWN_MOWER_ACTIONS: Partial<
Record<LawnMowerEntityState, LawnMowerAction>
> = {
mowing: {
action: "dock",
service: "dock",
feature: LawnMowerEntityFeature.DOCK,
},
docked: {
action: "start_mowing",
service: "start_mowing",
feature: LawnMowerEntityFeature.START_MOWING,
},
paused: {
action: "resume_mowing",
service: "start_mowing",
feature: LawnMowerEntityFeature.START_MOWING,
},
};
@customElement("ha-lawn_mower-action-button")
class HaLawnMowerActionButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: LawnMowerEntity;
public render() {
const state = this.stateObj.state;
const action = LAWN_MOWER_ACTIONS[state];
if (action && supportsFeature(this.stateObj, action.feature)) {
return html`
<mwc-button @click=${this.callService} .service=${action.service}>
${this.hass.localize(`ui.card.lawn_mower.actions.${action.action}`)}
</mwc-button>
`;
}
return html`
<mwc-button disabled>
${this.hass.formatEntityState(this.stateObj)}
</mwc-button>
`;
}
callService(ev) {
ev.stopPropagation();
const stateObj = this.stateObj;
const service = ev.target.service;
this.hass.callService("lawn_mower", service, {
entity_id: stateObj.entity_id,
});
}
static get styles(): CSSResultGroup {
return css`
mwc-button {
top: 3px;
height: 37px;
margin-right: -0.57em;
}
mwc-button[disabled] {
background-color: transparent;
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-lawn_mower-action-button": HaLawnMowerActionButton;
}
}

View File

@@ -1,5 +1,5 @@
import { mdiImagePlus } from "@mdi/js";
import { html, LitElement, TemplateResult } from "lit";
import { LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
@@ -9,6 +9,7 @@ import {
showImageCropperDialog,
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { HomeAssistant } from "../types";
import "./ha-button";
import "./ha-circular-progress";
import "./ha-file-upload";
@@ -20,6 +21,12 @@ export class HaPictureUpload extends LitElement {
@property() public label?: string;
@property() public secondary?: string;
@property() public supports?: string;
@property() public currentImageAltText?: string;
@property({ type: Boolean }) public crop = false;
@property({ attribute: false }) public cropOptions?: CropOptions;
@@ -29,19 +36,44 @@ export class HaPictureUpload extends LitElement {
@state() private _uploading = false;
public render(): TemplateResult {
return html`
<ha-file-upload
.hass=${this.hass}
.icon=${mdiImagePlus}
.label=${this.label ||
this.hass.localize("ui.components.picture-upload.label")}
.uploading=${this._uploading}
.value=${this.value ? html`<img .src=${this.value} />` : ""}
@file-picked=${this._handleFilePicked}
@change=${this._handleFileCleared}
accept="image/png, image/jpeg, image/gif"
></ha-file-upload>
`;
if (!this.value) {
return html`
<ha-file-upload
.hass=${this.hass}
.icon=${mdiImagePlus}
.label=${this.label ||
this.hass.localize("ui.components.picture-upload.label")}
.secondary=${this.secondary}
.supports=${this.supports ||
this.hass.localize("ui.components.picture-upload.supported_formats")}
.uploading=${this._uploading}
@file-picked=${this._handleFilePicked}
@change=${this._handleFileCleared}
accept="image/png, image/jpeg, image/gif"
></ha-file-upload>
`;
}
return html`<div class="center-vertical">
<div class="value">
<img
.src=${this.value}
alt=${this.currentImageAltText ||
this.hass.localize("ui.components.picture-upload.current_image_alt")}
/>
<ha-button
@click=${this._handleChangeClick}
.label=${this.hass.localize(
"ui.components.picture-upload.change_picture"
)}
>
</ha-button>
</div>
</div>`;
}
private _handleChangeClick() {
this.value = null;
fireEvent(this, "change");
}
private async _handleFilePicked(ev) {
@@ -100,6 +132,35 @@ export class HaPictureUpload extends LitElement {
this._uploading = false;
}
}
static get styles() {
return css`
:host {
display: block;
height: 240px;
}
ha-file-upload {
height: 100%;
}
.center-vertical {
display: flex;
align-items: center;
height: 100%;
}
.value {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
img {
max-width: 100%;
max-height: 200px;
margin-bottom: 4px;
border-radius: var(--file-upload-image-border-radius);
}
`;
}
}
declare global {

View File

@@ -47,6 +47,9 @@ export class HaSelect extends SelectBase {
.mdc-select__anchor {
width: var(--ha-select-min-width, 200px);
}
.mdc-select--filled .mdc-select__anchor {
height: var(--ha-select-height, 56px);
}
.mdc-select--filled .mdc-floating-label {
inset-inline-start: 12px;
inset-inline-end: initial;

View File

@@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { Action } from "../../data/script";
import { ActionSelector } from "../../data/selector";
@@ -19,10 +19,13 @@ export class HaActionSelector extends LitElement {
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-action
.disabled=${this.disabled}
.actions=${this.value || []}
.hass=${this.hass}
.nested=${this.selector.action?.nested}
.reOrderMode=${this.selector.action?.reorder_mode}
></ha-automation-action>
`;
}
@@ -37,6 +40,11 @@ export class HaActionSelector extends LitElement {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
`;
}
}

View File

@@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { Condition } from "../../data/automation";
import { ConditionSelector } from "../../data/selector";
@@ -19,10 +19,13 @@ export class HaConditionSelector extends LitElement {
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-condition
.disabled=${this.disabled}
.conditions=${this.value || []}
.hass=${this.hass}
.nested=${this.selector.condition?.nested}
.reOrderMode=${this.selector.condition?.reorder_mode}
></ha-automation-condition>
`;
}
@@ -37,6 +40,11 @@ export class HaConditionSelector extends LitElement {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
`;
}
}

View File

@@ -37,9 +37,12 @@ export class HaFileSelector extends LitElement {
.label=${this.label}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
.supports=${this.helper}
.uploading=${this._busy}
.value=${this.value ? this._filename?.name || "Unknown file" : ""}
.value=${this.value
? this._filename?.name ||
this.hass.localize("ui.components.selectors.file.unknown_file")
: undefined}
@file-picked=${this._uploadFile}
@change=${this._removeFile}
></ha-file-upload>

View File

@@ -1075,7 +1075,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
background-color: var(--accent-color);
line-height: 20px;
text-align: center;
padding: 0px 6px;
padding: 0px 2px;
color: var(--text-accent-color, var(--text-primary-color));
}
ha-svg-icon + .notification-badge,

View File

@@ -65,6 +65,7 @@ export class HaTabs extends PaperTabs {
const selected = this.querySelector(".iron-selected");
if (selected) {
selected.scrollIntoView();
this._affectScroll(0); // Ensure scroll arrows match scroll position
}
}

View File

@@ -1,436 +0,0 @@
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
import { css, html, LitElement, PropertyValues, svg } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { rgb2hex } from "../common/color/convert-color";
import { temperature2rgb } from "../common/color/convert-light-color";
import { fireEvent } from "../common/dom/fire_event";
const SAFE_ZONE_FACTOR = 0.9;
declare global {
interface HASSDomEvents {
"cursor-moved": { value?: any };
}
}
const A11Y_KEY_CODES = new Set([
"ArrowRight",
"ArrowUp",
"ArrowLeft",
"ArrowDown",
"PageUp",
"PageDown",
"Home",
"End",
]);
function xy2polar(x: number, y: number) {
const r = Math.sqrt(x * x + y * y);
const phi = Math.atan2(y, x);
return [r, phi];
}
function polar2xy(r: number, phi: number) {
const x = Math.cos(phi) * r;
const y = Math.sin(phi) * r;
return [x, y];
}
function drawColorWheel(
ctx: CanvasRenderingContext2D,
minTemp: number,
maxTemp: number
) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const radius = ctx.canvas.width / 2;
const min = Math.max(minTemp, 2000);
const max = Math.min(maxTemp, 40000);
for (let y = -radius; y < radius; y += 1) {
const x = radius * Math.sqrt(1 - (y / radius) ** 2);
const fraction = (y / (radius * SAFE_ZONE_FACTOR) + 1) / 2;
const temperature = Math.max(
Math.min(min + fraction * (max - min), max),
min
);
const color = rgb2hex(temperature2rgb(temperature));
ctx.fillStyle = color;
ctx.fillRect(radius - x, radius + y - 0.5, 2 * x, 2);
ctx.fill();
}
}
@customElement("ha-temp-color-picker")
class HaTempColorPicker extends LitElement {
@property({ type: Boolean, reflect: true })
public disabled = false;
@property({ type: Number, attribute: false })
public renderSize?: number;
@property({ type: Number })
public value?: number;
@property({ type: Number })
public min = 2000;
@property({ type: Number })
public max = 10000;
@query("#canvas") private _canvas!: HTMLCanvasElement;
private _mc?: HammerManager;
@state()
private _pressed?: string;
@state()
private _cursorPosition?: [number, number];
@state()
private _localValue?: number;
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._setupListeners();
this._generateColorWheel();
this.setAttribute("role", "slider");
this.setAttribute("aria-orientation", "vertical");
if (!this.hasAttribute("tabindex")) {
this.setAttribute("tabindex", "0");
}
}
private _generateColorWheel() {
const ctx = this._canvas.getContext("2d")!;
drawColorWheel(ctx, this.min, this.max);
}
connectedCallback(): void {
super.connectedCallback();
this._setupListeners();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._destroyListeners();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("_localValue")) {
this.setAttribute("aria-valuenow", this._localValue?.toString() ?? "");
}
if (changedProps.has("min") || changedProps.has("max")) {
this._generateColorWheel();
this._resetPosition();
}
if (changedProps.has("min")) {
this.setAttribute("aria-valuemin", this.min.toString());
}
if (changedProps.has("max")) {
this.setAttribute("aria-valuemax", this.max.toString());
}
if (changedProps.has("value")) {
if (this._localValue !== this.value) {
this._resetPosition();
}
}
}
private _setupListeners() {
if (this._canvas && !this._mc) {
this._mc = new Manager(this._canvas);
this._mc.add(
new Pan({
direction: DIRECTION_ALL,
enable: true,
threshold: 0,
})
);
this._mc.add(new Tap({ event: "singletap" }));
let savedPosition;
this._mc.on("panstart", (e) => {
if (this.disabled) return;
this._pressed = e.pointerType;
savedPosition = this._cursorPosition;
});
this._mc.on("pancancel", () => {
if (this.disabled) return;
this._pressed = undefined;
this._cursorPosition = savedPosition;
});
this._mc.on("panmove", (e) => {
if (this.disabled) return;
this._cursorPosition = this._getPositionFromEvent(e);
this._localValue = this._getValueFromCoord(...this._cursorPosition);
fireEvent(this, "cursor-moved", { value: this._localValue });
});
this._mc.on("panend", (e) => {
if (this.disabled) return;
this._pressed = undefined;
this._cursorPosition = this._getPositionFromEvent(e);
this._localValue = this._getValueFromCoord(...this._cursorPosition);
fireEvent(this, "cursor-moved", { value: undefined });
fireEvent(this, "value-changed", { value: this._localValue });
});
this._mc.on("singletap", (e) => {
if (this.disabled) return;
this._cursorPosition = this._getPositionFromEvent(e);
this._localValue = this._getValueFromCoord(...this._cursorPosition);
fireEvent(this, "value-changed", { value: this._localValue });
});
this.addEventListener("keydown", this._handleKeyDown);
this.addEventListener("keyup", this._handleKeyUp);
}
}
private _resetPosition() {
if (this.value === undefined) {
this._cursorPosition = undefined;
this._localValue = undefined;
return;
}
const [, y] = this._getCoordsFromValue(this.value);
const currentX = this._cursorPosition?.[0] ?? 0;
const x =
Math.sign(currentX) * Math.min(Math.sqrt(1 - y ** 2), Math.abs(currentX));
this._cursorPosition = [x, y];
this._localValue = this.value;
}
private _getCoordsFromValue = (temperature: number): [number, number] => {
if (this.value === this.min) {
return [0, -1];
}
if (this.value === this.max) {
return [0, 1];
}
const fraction = (temperature - this.min) / (this.max - this.min);
const y = (2 * fraction - 1) * SAFE_ZONE_FACTOR;
return [0, y];
};
private _getValueFromCoord = (_x: number, y: number): number => {
const fraction = (y / SAFE_ZONE_FACTOR + 1) / 2;
const temperature = Math.max(
Math.min(this.min + fraction * (this.max - this.min), this.max),
this.min
);
return Math.round(temperature);
};
private _getPositionFromEvent = (e: HammerInput): [number, number] => {
const x = e.center.x;
const y = e.center.y;
const boundingRect = e.target.getBoundingClientRect();
const offsetX = boundingRect.left;
const offsetY = boundingRect.top;
const maxX = e.target.clientWidth;
const maxY = e.target.clientHeight;
const _x = (2 * (x - offsetX)) / maxX - 1;
const _y = (2 * (y - offsetY)) / maxY - 1;
const [r, phi] = xy2polar(_x, _y);
const [__x, __y] = polar2xy(Math.min(1, r), phi);
return [__x, __y];
};
private _destroyListeners() {
if (this._mc) {
this._mc.destroy();
this._mc = undefined;
}
this.removeEventListener("keydown", this._handleKeyDown);
this.removeEventListener("keyup", this._handleKeyDown);
}
_handleKeyDown(e: KeyboardEvent) {
if (!A11Y_KEY_CODES.has(e.code)) return;
e.preventDefault();
const step = 1;
const tenPercentStep = Math.max(step, (this.max - this.min) / 10);
const currentValue =
this._localValue ?? Math.round((this.max + this.min) / 2);
switch (e.code) {
case "ArrowRight":
case "ArrowUp":
this._localValue = Math.round(Math.min(currentValue + step, this.max));
break;
case "ArrowLeft":
case "ArrowDown":
this._localValue = Math.round(Math.max(currentValue - step, this.min));
break;
case "PageUp":
this._localValue = Math.round(
Math.min(currentValue + tenPercentStep, this.max)
);
break;
case "PageDown":
this._localValue = Math.round(
Math.max(currentValue - tenPercentStep, this.min)
);
break;
case "Home":
this._localValue = this.min;
break;
case "End":
this._localValue = this.max;
break;
}
if (this._localValue != null) {
const [_, y] = this._getCoordsFromValue(this._localValue);
const currentX = this._cursorPosition?.[0] ?? 0;
const x =
Math.sign(currentX) *
Math.min(Math.sqrt(1 - y ** 2), Math.abs(currentX));
this._cursorPosition = [x, y];
fireEvent(this, "cursor-moved", { value: this._localValue });
}
}
_handleKeyUp(e: KeyboardEvent) {
if (!A11Y_KEY_CODES.has(e.code)) return;
e.preventDefault();
this.value = this._localValue;
fireEvent(this, "value-changed", { value: this._localValue });
}
render() {
const size = this.renderSize || 400;
const canvasSize = size * window.devicePixelRatio;
const rgb = temperature2rgb(
this._localValue ?? Math.round((this.max + this.min) / 2)
);
const [x, y] = this._cursorPosition ?? [0, 0];
const cx = ((x + 1) * size) / 2;
const cy = ((y + 1) * size) / 2;
const markerPosition = `${cx}px, ${cy}px`;
const markerScale = this._pressed
? this._pressed === "touch"
? "2.5"
: "1.5"
: "1";
const markerOffset =
this._pressed === "touch" ? `0px, -${size / 16}px` : "0px, 0px";
return html`
<div class="container ${classMap({ pressed: Boolean(this._pressed) })}">
<canvas id="canvas" .width=${canvasSize} .height=${canvasSize}></canvas>
<svg
id="interaction"
viewBox="0 0 ${size} ${size}"
overflow="visible"
aria-hidden="true"
>
<defs>${this.renderSVGFilter()}</defs>
<g
style=${styleMap({
fill: rgb2hex(rgb),
transform: `translate(${markerPosition})`,
})}
class="cursor"
>
<circle
cx="0"
cy="0"
r="16"
style=${styleMap({
fill: rgb2hex(rgb),
transform: `translate(${markerOffset}) scale(${markerScale})`,
visibility: this._cursorPosition ? undefined : "hidden",
})}
></circle>
</g>
</svg>
</div>
`;
}
renderSVGFilter() {
return svg`
<filter
id="marker-shadow"
x="-50%"
y="-50%"
width="200%"
height="200%"
filterUnits="objectBoundingBox"
>
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.3" flood-color="rgba(0, 0, 0, 1)"/>
<feDropShadow dx="0" dy="1" stdDeviation="3" flood-opacity="0.15" flood-color="rgba(0, 0, 0, 1)"/>
</filter>
`;
}
static get styles() {
return css`
:host {
display: block;
outline: none;
}
.container {
position: relative;
width: 100%;
height: 100%;
display: flex;
}
canvas {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 50%;
transition: box-shadow 180ms ease-in-out;
cursor: pointer;
}
:host(:focus-visible) canvas {
box-shadow: 0 0 0 2px rgb(255, 160, 0);
}
svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
circle {
fill: black;
stroke: white;
stroke-width: 2;
filter: url(#marker-shadow);
}
.container:not(.pressed) circle {
transition:
transform 100ms ease-in-out,
fill 100ms ease-in-out;
}
.container:not(.pressed) .cursor {
transition: transform 200ms ease-in-out;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-temp-color-picker": HaTempColorPicker;
}
}

View File

@@ -1,7 +1,6 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/number/format_number";
import LocalizeMixin from "../mixins/localize-mixin";
@@ -84,12 +83,7 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
}
_localizeState(stateObj) {
return computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.locale,
this.hass.entities
);
return this.hass.formatEntityState(stateObj);
}
}
customElements.define("ha-water_heater-state", HaWaterHeaterState);

View File

@@ -102,9 +102,8 @@ class HaWebRtcPlayer extends LitElement {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
const offer: RTCSessionDescriptionInit = await peerConnection.createOffer(
offerOptions
);
const offer: RTCSessionDescriptionInit =
await peerConnection.createOffer(offerOptions);
await peerConnection.setLocalDescription(offer);
let candidates = ""; // Build an Offer SDP string with ice candidates

View File

@@ -1,15 +0,0 @@
import { HomeAssistant } from "../types";
export const createLanguageListEl = (hass: HomeAssistant) => {
const list = document.createElement("datalist");
list.id = "languages";
for (const [language, metadata] of Object.entries(
hass.translationMetadata.translations
)) {
const option = document.createElement("option");
option.value = language;
option.innerText = metadata.nativeName;
list.appendChild(option);
}
return list;
};

View File

@@ -122,6 +122,7 @@ export class HaMediaPlayerBrowse extends LitElement {
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._resizeObserver) {
this._resizeObserver.disconnect();
}

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