Compare commits

..

134 Commits

Author SHA1 Message Date
Bram Kragten
7f2fcc73b5 Delay display of init page 2023-09-20 17:05:27 +02:00
Paul Bottein
4b5c7021ff Add select option tile feature (#17971) 2023-09-20 12:43:21 +02:00
Paul Bottein
3349031cbd Simplify data table template (#17825)
* Simplify data table template

* Fix backup and gallery
2023-09-20 12:09:44 +02:00
renovate[bot]
5e107d43d7 Update babel monorepo to v7.22.20 (#17965)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 14:36:07 -04:00
Steve Repsher
e46f2cd9bf Adjust password manager polyfill for form injections (#17830) 2023-09-19 14:51:38 +02:00
Steve Repsher
713ebfcc22 Enable shipped proposals in Babel (#17909) 2023-09-19 13:56:55 +02:00
renovate[bot]
46e4eafe95 Update dependency @types/babel__plugin-transform-runtime to v7.9.3 (#17957) 2023-09-19 07:45:50 -04:00
renovate[bot]
e6fd18e23b Update dependency @types/js-yaml to v4.0.6 (#17958) 2023-09-19 07:43:55 -04:00
renovate[bot]
71cd71dfd5 Update formatjs monorepo (#17890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 12:50:43 -04:00
renovate[bot]
1019ccfd26 Update dependency @lrnwebcomponents/simple-tooltip to v7.0.18 (#17905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 12:41:29 -04:00
renovate[bot]
577c1d8522 Update dependency @lit-labs/context to v0.4.1 (#17935)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 12:35:46 -04:00
renovate[bot]
63f0b469cc Update dependency vis-data to v7.1.7 (#17939) 2023-09-18 12:25:23 -04:00
renovate[bot]
e688417863 Update dependency @babel/core to v7.22.19 (#17948) 2023-09-18 08:27:41 -04:00
renovate[bot]
a19633e2d4 Update CodeMirror (#17944) 2023-09-18 08:21:31 -04:00
renovate[bot]
8797142cca Update dependency sinon to v16 (#17940) 2023-09-18 08:15:40 -04:00
Madelena Mak
2a7403b6fd Added new logo design guidelines (#17951)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-09-17 22:29:53 +02:00
renovate[bot]
22efe14149 Update dependency @types/tar to v6.1.6 (#17931)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-15 18:57:25 -04:00
renovate[bot]
7cce24bcd1 Update typescript-eslint monorepo to v6.7.0 (#17923) 2023-09-14 20:25:46 -04:00
renovate[bot]
b8f0bb66cd Update dependency @codemirror/view to v6.18.1 (#17922)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-14 13:11:12 -04:00
renovate[bot]
b950f990b4 Update dependency @material/web to v1.0.0-pre.17 (#17882)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-13 10:11:13 +02:00
renovate[bot]
b511e7a37d Update vaadinWebComponents monorepo to v24.1.7 (#17894) 2023-09-12 20:38:23 -04:00
renovate[bot]
50f4b78f2e Update dependency ua-parser-js to v1.0.36 (#17912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 11:45:17 -04:00
renovate[bot]
7b0b4cdfe4 Update dependency @codemirror/view to v6.18.0 (#17865) 2023-09-11 20:59:49 -04:00
renovate[bot]
c60e5c4c61 Update dependency eslint to v8.49.0 (#17904) 2023-09-11 20:41:20 -04:00
karwosts
709a63e6da Fix schedule helper form when resizing or dragging to or past midnight (#17900) 2023-09-11 22:54:45 +02:00
dependabot[bot]
f689eed073 Bump actions/checkout from 3.6.0 to 4.0.0 (#17891)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.6.0 to 4.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.6.0...v4.0.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 15:22:18 -04:00
renovate[bot]
cd55eee2fc Update dependency @babel/core to v7.22.17 (#17899)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-11 15:19:05 -04:00
dependabot[bot]
cf27e68748 Bump actions/cache from 3.3.1 to 3.3.2 (#17892)
Bumps [actions/cache](https://github.com/actions/cache) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.3.1...v3.3.2)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 12:24:36 -04:00
Bram Kragten
472ed2fe82 Bumped version to 20230911.0 2023-09-11 18:19:34 +02:00
Paul Bottein
d0a60984ed Allow user to theme all cover states in tile card and more info. (#17898) 2023-09-11 18:18:39 +02:00
Bram Kragten
24d401061c Show error when set state fails (#17850) 2023-09-11 18:13:36 +02:00
Bram Kragten
2352d05573 Ignore errors during logging (#17893) 2023-09-11 13:24:08 +02:00
renovate[bot]
87d53e38c4 Update dependency tar to v6.2.0 (#17861)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-10 19:21:53 -04:00
renovate[bot]
db3c535884 Update dependency core-js to v3.32.2 (#17887)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-10 19:18:23 -04:00
renovate[bot]
158b24f902 Update dependency luxon to v3.4.3 (#17872)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-10 19:16:40 -04:00
Joakim Sørensen
19c4ed4690 Show alternative names in certificate dialog (#17839) 2023-09-08 19:55:27 +02:00
Bram Kragten
eae4ca1271 if select has multiple, make init value an array (#17868)
if multiple, make it an array
2023-09-08 19:46:40 +02:00
Bram Kragten
0276430ab5 Bumped version to 20230908.0 2023-09-08 15:55:22 +02:00
karwosts
db7caf1c32 Fix default formatEntityState function (#17852) 2023-09-08 15:54:54 +02:00
karwosts
7176a51fec update hasChanged for formatEntityState, fix glance card shouldUpdate (#17854) 2023-09-08 15:54:11 +02:00
Bram Kragten
4a6539d75b Clear template result when template changes (#17849) 2023-09-07 20:48:57 -04:00
renovate[bot]
850699ea70 Update dependency @types/chromecast-caf-sender to v1.0.6 (#17857) 2023-09-08 00:01:27 +00:00
renovate[bot]
c17cc22f88 Update typescript-eslint monorepo to v6.6.0 (#17858) 2023-09-07 19:59:46 -04:00
renovate[bot]
9e3f2d5cb7 Update babel monorepo to v7.22.15 (#17853) 2023-09-07 19:56:59 -04:00
renovate[bot]
0677c9c7b0 Update dependency @types/chromecast-caf-receiver to v6.0.10 (#17856) 2023-09-07 19:47:22 -04:00
Bram Kragten
af7e385884 Fix tooltip graph overflow (#17848) 2023-09-07 19:12:35 +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
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
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
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
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
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
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
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
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
211 changed files with 4733 additions and 5044 deletions

View File

@@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
with: with:
ref: dev ref: dev
@@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
with: with:
ref: master ref: master

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:
@@ -37,7 +37,7 @@ jobs:
- name: Build resources - name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache - name: Setup lint cache
uses: actions/cache@v3.3.1 uses: actions/cache@v3.3.2
with: with:
path: | path: |
node_modules/.cache/prettier node_modules/.cache/prettier
@@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:
@@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:
@@ -91,7 +91,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:

View File

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

View File

@@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
with: with:
ref: dev ref: dev
@@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
with: with:
ref: master ref: master

View File

@@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1

View File

@@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1

View File

@@ -20,7 +20,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4 uses: actions/setup-python@v4

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v4.0.0
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master

View File

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

View File

@@ -100,6 +100,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
useBuiltIns: latestBuild ? false : "entry", useBuiltIns: latestBuild ? false : "entry",
corejs: latestBuild ? false : { version: "3.32", proposals: true }, corejs: latestBuild ? false : { version: "3.32", proposals: true },
bugfixes: true, bugfixes: true,
shippedProposals: true,
}, },
], ],
"@babel/preset-typescript", "@babel/preset-typescript",

View File

@@ -1,10 +1,14 @@
import fs from "fs/promises"; import fs from "fs/promises";
import gulp from "gulp"; import gulp from "gulp";
import path from "path";
import mapStream from "map-stream"; import mapStream from "map-stream";
import transform from "gulp-json-transform"; import transform from "gulp-json-transform";
import { LokaliseApi } from "@lokalise/node-api";
import JSZip from "jszip";
const inDirFrontend = "translations/frontend"; const inDir = "translations";
const inDirBackend = "translations/backend"; const inDirFrontend = `${inDir}/frontend`;
const inDirBackend = `${inDir}/backend`;
const srcMeta = "src/translations/translationMetadata.json"; const srcMeta = "src/translations/translationMetadata.json";
const encoding = "utf8"; const encoding = "utf8";
@@ -68,8 +72,9 @@ gulp.task("convert-backend-translations", function () {
}); });
gulp.task("check-translations-html", function () { gulp.task("check-translations-html", function () {
// We exclude backend translations because they are not compliant with the HTML rule for now return gulp
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml()); .src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`])
.pipe(checkHtml());
}); });
gulp.task("check-all-files-exist", async function () { gulp.task("check-all-files-exist", async function () {
@@ -89,7 +94,83 @@ gulp.task("check-all-files-exist", async function () {
await Promise.allSettled(writings); 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( gulp.task(
"check-downloaded-translations", "download-translations",
gulp.series("check-translations-html", "check-all-files-exist") gulp.series(
"fetch-lokalise",
"convert-backend-translations",
"check-translations-html",
"check-all-files-exist"
)
); );

View File

@@ -6,6 +6,8 @@ import presetEnv from "@babel/preset-env";
import compilationTargets from "@babel/helper-compilation-targets"; import compilationTargets from "@babel/helper-compilation-targets";
import coreJSCompat from "core-js-compat"; import coreJSCompat from "core-js-compat";
import { logPlugin } from "@babel/preset-env/lib/debug.js"; import { logPlugin } from "@babel/preset-env/lib/debug.js";
// eslint-disable-next-line import/no-relative-packages
import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js";
import { babelOptions } from "./bundle.cjs"; import { babelOptions } from "./bundle.cjs";
const detailsOpen = (heading) => const detailsOpen = (heading) =>
@@ -26,6 +28,22 @@ const dummyAPI = {
targets: () => ({}), targets: () => ({}),
}; };
// Generate filter function based on proposal/method inputs
// Copied and adapted from babel-plugin-polyfill-corejs3/esm/index.mjs
const polyfillFilter = (method, proposals, shippedProposals) => (name) => {
if (proposals || method === "entry-global") return true;
if (shippedProposals && shippedPolyfills.default.has(name)) {
return true;
}
if (name.startsWith("esnext.")) {
const esName = `es.${name.slice(7)}`;
// If its imaginative esName is not in latest compat data, it means the proposal is not stage 4
return esName in coreJSCompat.data;
}
return true;
};
// Log the plugins and polyfills for each build environment
for (const buildType of ["Modern", "Legacy"]) { for (const buildType of ["Modern", "Legacy"]) {
const browserslistEnv = buildType.toLowerCase(); const browserslistEnv = buildType.toLowerCase();
const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" }); const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" });
@@ -46,7 +64,13 @@ for (const buildType of ["Modern", "Legacy"]) {
const targets = compilationTargets.default(babelOpts?.targets, { const targets = compilationTargets.default(babelOpts?.targets, {
browserslistEnv, browserslistEnv,
}); });
const polyfillList = coreJSCompat({ targets }).list; const polyfillList = coreJSCompat({ targets }).list.filter(
polyfillFilter(
`${presetEnvOpts.useBuiltIns}-global`,
presetEnvOpts?.corejs?.proposals,
presetEnvOpts?.shippedProposals
)
);
console.log( console.log(
"The following %i polyfills may be injected by Babel:\n", "The following %i polyfills may be injected by Babel:\n",
polyfillList.length polyfillList.length

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
# Note!
Note, the assets in this folder, are not part of the CC license this repository is shipped in.
All rights reserved.

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -2,30 +2,86 @@
title: "Logo" title: "Logo"
--- ---
# Using our logo # Our logo
As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color. As a community, we are proud of our logo. Follow these guidelines to ensure it always represents the identity of the Home Assistant project and community the best way possible.
[Download Logo](https://github.com/home-assistant/assets/tree/master/logo) [Download Logo](https://github.com/home-assistant/assets/tree/master/logo)
![Logo](/images/logo.png) ![Logo](/images/brand/logo.png)
## Using the icon Please note that this logo is not released under the CC license. All rights reserved.
Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon. # Design
![Logo variants](/images/logo-variants.png) At the core of the Home Assistant logomark is the Blue House with Antenna, the three most recognizable and distinct features of the previous logo throughout the past decade.
## Using the right variant ### Blue
The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography. Blue feels stable and essential. A bright sky blue is joyful, clear, and free of clouds.
When needed you can use our logo without a shadow, as seen as the second variant. ### House
The outlined logo should only be used on packaging. Of all possible combinations of shapes, a home is best abstracted in the shape of a structure with a pitched roof. With the vast amount of logos based on this shape, the best we can do is to make it more iconic. The house is further simplified - there is no gable and there is no chimney - to an orthogonal shape with an elegant and deliberate proportion.
## Exclusion zone ### Antenna
The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon. Call it a tree, a set of nodes, a PCB, or an antenna. The antenna is the most recognizable and memorable part of the previous Home Assistant logo, and is an easily understandable symbol that conveys technologies that are smart, connected, and growing evergreen.
![Clearspace](/images/clearspace.png) # Usage
The default variation is the static colored wordmark in horizontal layout and dark text on a light background.
## Layout variations
![Logo layout variants](/images/brand/logo-layout-variants.png)
The default layout is the wordmark in horizontal layout. It provides the clearest context to the brand identity of Home Assistant.
Use the logomark variant when the context is clear that the logo is about Home Assistant. For example, inside the Home Assistant app where users are already aware of where they are at, the logomark variant without the wordmark can be used. The logomark can exist without the wordmark, however, the wordmark should never exist without the icon.
Use the wordmark in vertical layout when the space available has an aspect ratio less than 4:3. For example, in a square space on a t-shirt where a logo is needed, since there is no established context of Home Assistant, the wordmark in vertical layout should be used.
Lastly, use the wordmark in vertical layout with small logomark when Home Assistant is displayed in context of other Home Assistant-related projects. For example, in a flowchart showing the voice pipeline, use this layout for Home Assistant and its other related projects.
## Color variations, backgrounds, and placement
The default color is the colored version on light background with dark text.
For backgrounds that are dark, for example, when it is used on a page in a dark theme, use the colored version on dark background with light text.
In printed materials where color is unavailable, use the monochrome color variations.
On background that are dark or photographic, use the light monochrome color on dark background variation.
On backgrounds that are light or photographic, use the colored version. Do not use the monochrome variations.
Do not enclose the logmark in a square or color or any confined backgrounds, except in specific situations enforced by another company's marketplace guidelines, for example, an iOS app icon.
Do not add drop shadow to the logomark or the wordmark. If legibility is compromised due to the background, change the background to provide more contrast, or in last resort, add a heavily blurred drop shadaow.
It should only be used with black, white, and non-duotone photography.
Unlike the previous version of our logo, no outlined variants are available. Use the monochrome variants in those spaces.
### Exclusion zone
The logo needs some personal space. Its exclusion zone is equal to a quarter the height of the icon.
![Space clearance for the wordmark](/images/brand/logo-exclusion-zone.png)
## Animation
The default is the static variant.
Use the animated variant only for introductory purposes, for example, in the beginning of a video or on a loading screen.
Use the animated with sound variant only when sound is warranted in the user's context. For example, use it in the beginning of a video since sounds are expected in a video, but do not use it on a loading screen since sounds are not expected in a user interface.
Do not repeat the logo animation.
## Sizes and app icon variants
Special variants are created for specific contexts.
Use the tiny variants when the logomark is used in a very small space (16x16 dp), for example, the favicon of the Home Assistant website, a notification on Android, or the menubar of macOS.

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

@@ -343,7 +343,7 @@ export class DemoEntityState extends LitElement {
const columns: DataTableColumnContainer<EntityRowData> = { const columns: DataTableColumnContainer<EntityRowData> = {
icon: { icon: {
title: "Icon", title: "Icon",
template: (_, entry) => html` template: (entry) => html`
<state-badge <state-badge
.stateObj=${entry.stateObj} .stateObj=${entry.stateObj}
.stateColor=${true} .stateColor=${true}
@@ -360,7 +360,7 @@ export class DemoEntityState extends LitElement {
title: "State", title: "State",
width: "20%", width: "20%",
sortable: true, sortable: true,
template: (_, entry) => template: (entry) =>
html`${computeStateDisplay( html`${computeStateDisplay(
hass.localize, hass.localize,
entry.stateObj, entry.stateObj,
@@ -371,14 +371,14 @@ export class DemoEntityState extends LitElement {
}, },
device_class: { device_class: {
title: "Device class", title: "Device class",
template: (dc) => html`${dc ?? "-"}`, template: (entry) => html`${entry.device_class ?? "-"}`,
width: "20%", width: "20%",
filterable: true, filterable: true,
sortable: true, sortable: true,
}, },
domain: { domain: {
title: "Domain", title: "Domain",
template: (_, entry) => html`${computeDomain(entry.entity_id)}`, template: (entry) => html`${computeDomain(entry.entity_id)}`,
width: "20%", width: "20%",
filterable: true, filterable: true,
sortable: true, sortable: true,

View File

@@ -49,6 +49,10 @@ import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hass
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
type BackupItem = HassioBackup & {
secondary: string;
};
@customElement("hassio-backups") @customElement("hassio-backups")
export class HassioBackups extends LitElement { export class HassioBackups extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -117,15 +121,15 @@ export class HassioBackups extends LitElement {
} }
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer => ({ (narrow: boolean): DataTableColumnContainer<BackupItem> => ({
name: { name: {
title: this.supervisor.localize("backup.name"), title: this.supervisor.localize("backup.name"),
main: true, main: true,
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true, grows: true,
template: (entry: string, backup: any) => template: (backup) =>
html`${entry || backup.slug} html`${backup.name || backup.slug}
<div class="secondary">${backup.secondary}</div>`, <div class="secondary">${backup.secondary}</div>`,
}, },
size: { size: {
@@ -134,7 +138,7 @@ export class HassioBackups extends LitElement {
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,
sortable: true, sortable: true,
template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB", template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
}, },
location: { location: {
title: this.supervisor.localize("backup.location"), title: this.supervisor.localize("backup.location"),
@@ -142,8 +146,8 @@ export class HassioBackups extends LitElement {
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,
sortable: true, sortable: true,
template: (entry: string | null) => template: (backup) =>
entry || this.supervisor.localize("backup.data_disk"), backup.location || this.supervisor.localize("backup.data_disk"),
}, },
date: { date: {
title: this.supervisor.localize("backup.created"), title: this.supervisor.localize("backup.created"),
@@ -152,8 +156,8 @@ export class HassioBackups extends LitElement {
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,
sortable: true, sortable: true,
template: (entry: string) => template: (backup) =>
relativeTime(new Date(entry), this.hass.locale), relativeTime(new Date(backup.date), this.hass.locale),
}, },
secondary: { secondary: {
title: "", title: "",
@@ -163,7 +167,7 @@ export class HassioBackups extends LitElement {
}) })
); );
private _backupData = memoizeOne((backups: HassioBackup[]) => private _backupData = memoizeOne((backups: HassioBackup[]): BackupItem[] =>
backups.map((backup) => ({ backups.map((backup) => ({
...backup, ...backup,
secondary: this._computeBackupContent(backup), secondary: this._computeBackupContent(backup),

View File

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

View File

@@ -25,24 +25,24 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.22.11", "@babel/runtime": "7.22.15",
"@braintree/sanitize-url": "6.0.4", "@braintree/sanitize-url": "6.0.4",
"@codemirror/autocomplete": "6.9.0", "@codemirror/autocomplete": "6.9.1",
"@codemirror/commands": "6.2.4", "@codemirror/commands": "6.2.5",
"@codemirror/language": "6.9.0", "@codemirror/language": "6.9.0",
"@codemirror/legacy-modes": "6.3.3", "@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.1", "@codemirror/search": "6.5.3",
"@codemirror/state": "6.2.1", "@codemirror/state": "6.2.1",
"@codemirror/view": "6.16.0", "@codemirror/view": "6.19.0",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.10.0", "@formatjs/intl-datetimeformat": "6.10.2",
"@formatjs/intl-displaynames": "6.5.0", "@formatjs/intl-displaynames": "6.5.2",
"@formatjs/intl-getcanonicallocales": "2.2.1", "@formatjs/intl-getcanonicallocales": "2.2.1",
"@formatjs/intl-listformat": "7.4.0", "@formatjs/intl-listformat": "7.4.2",
"@formatjs/intl-locale": "3.3.2", "@formatjs/intl-locale": "3.3.4",
"@formatjs/intl-numberformat": "8.7.0", "@formatjs/intl-numberformat": "8.7.2",
"@formatjs/intl-pluralrules": "5.2.4", "@formatjs/intl-pluralrules": "5.2.6",
"@formatjs/intl-relativetimeformat": "11.2.4", "@formatjs/intl-relativetimeformat": "11.2.6",
"@fullcalendar/core": "6.1.8", "@fullcalendar/core": "6.1.8",
"@fullcalendar/daygrid": "6.1.8", "@fullcalendar/daygrid": "6.1.8",
"@fullcalendar/interaction": "6.1.8", "@fullcalendar/interaction": "6.1.8",
@@ -50,10 +50,10 @@
"@fullcalendar/luxon3": "6.1.8", "@fullcalendar/luxon3": "6.1.8",
"@fullcalendar/timegrid": "6.1.8", "@fullcalendar/timegrid": "6.1.8",
"@lezer/highlight": "1.1.6", "@lezer/highlight": "1.1.6",
"@lit-labs/context": "0.4.0", "@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.4", "@lit-labs/motion": "1.0.4",
"@lit-labs/virtualizer": "2.0.6", "@lit-labs/virtualizer": "2.0.7",
"@lrnwebcomponents/simple-tooltip": "7.0.16", "@lrnwebcomponents/simple-tooltip": "7.0.18",
"@material/chips": "=14.0.0-canary.53b3cad2f.0", "@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0", "@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-button": "0.27.0", "@material/mwc-button": "0.27.0",
@@ -79,10 +79,9 @@
"@material/mwc-top-app-bar": "0.27.0", "@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0", "@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.0-pre.16", "@material/web": "=1.0.0-pre.17",
"@mdi/js": "7.2.96", "@mdi/js": "7.2.96",
"@mdi/svg": "7.2.96", "@mdi/svg": "7.2.96",
"@polymer/app-layout": "3.1.0",
"@polymer/iron-flex-layout": "3.0.1", "@polymer/iron-flex-layout": "3.0.1",
"@polymer/iron-input": "3.0.1", "@polymer/iron-input": "3.0.1",
"@polymer/iron-resizable-behavior": "3.0.1", "@polymer/iron-resizable-behavior": "3.0.1",
@@ -94,8 +93,8 @@
"@polymer/paper-toast": "3.0.1", "@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1", "@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.1.6", "@vaadin/combo-box": "24.1.7",
"@vaadin/vaadin-themable-mixin": "24.1.6", "@vaadin/vaadin-themable-mixin": "24.1.7",
"@vibrant/color": "3.2.1-alpha.1", "@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1", "@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -103,26 +102,26 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9", "@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0", "@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1", "app-datepicker": "5.1.1",
"chart.js": "3.3.2", "chart.js": "4.3.3",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.32.1", "core-js": "3.32.2",
"cropperjs": "1.5.13", "cropperjs": "1.6.0",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"date-fns-tz": "2.0.0", "date-fns-tz": "2.0.0",
"deep-clone-simple": "1.1.1", "deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"fuse.js": "6.6.2", "fuse.js": "6.6.2",
"google-timezones-json": "1.2.0", "google-timezones-json": "1.2.0",
"hls.js": "1.4.10", "hls.js": "1.4.12",
"home-assistant-js-websocket": "8.2.0", "home-assistant-js-websocket": "8.2.0",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"intl-messageformat": "10.5.0", "intl-messageformat": "10.5.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"leaflet": "1.9.4", "leaflet": "1.9.4",
"leaflet-draw": "1.0.4", "leaflet-draw": "1.0.4",
"lit": "2.8.0", "lit": "2.8.0",
"luxon": "3.4.2", "luxon": "3.4.3",
"marked": "7.0.4", "marked": "7.0.5",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2", "proxy-polyfill": "0.3.2",
@@ -138,9 +137,9 @@
"tinykeys": "2.1.0", "tinykeys": "2.1.0",
"tsparticles-engine": "2.12.0", "tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0", "tsparticles-preset-links": "2.12.0",
"ua-parser-js": "1.0.35", "ua-parser-js": "1.0.36",
"unfetch": "5.0.0", "unfetch": "5.0.0",
"vis-data": "7.1.6", "vis-data": "7.1.7",
"vis-network": "9.1.6", "vis-network": "9.1.6",
"vue": "2.7.14", "vue": "2.7.14",
"vue2-daterange-picker": "0.6.8", "vue2-daterange-picker": "0.6.8",
@@ -154,12 +153,13 @@
"xss": "1.0.14" "xss": "1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.22.11", "@babel/core": "7.22.20",
"@babel/plugin-proposal-decorators": "7.22.10", "@babel/plugin-proposal-decorators": "7.22.15",
"@babel/plugin-transform-runtime": "7.22.10", "@babel/plugin-transform-runtime": "7.22.15",
"@babel/preset-env": "7.22.10", "@babel/preset-env": "7.22.20",
"@babel/preset-typescript": "7.22.11", "@babel/preset-typescript": "7.22.15",
"@koa/cors": "4.0.0", "@koa/cors": "4.0.0",
"@lokalise/node-api": "11.0.1",
"@octokit/auth-oauth-device": "6.0.0", "@octokit/auth-oauth-device": "6.0.0",
"@octokit/plugin-retry": "6.0.0", "@octokit/plugin-retry": "6.0.0",
"@octokit/rest": "20.0.1", "@octokit/rest": "20.0.1",
@@ -169,32 +169,32 @@
"@rollup/plugin-json": "6.0.0", "@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.2.1", "@rollup/plugin-node-resolve": "15.2.1",
"@rollup/plugin-replace": "5.0.2", "@rollup/plugin-replace": "5.0.2",
"@types/babel__plugin-transform-runtime": "7.9.2", "@types/babel__plugin-transform-runtime": "7.9.3",
"@types/chromecast-caf-receiver": "6.0.9", "@types/chromecast-caf-receiver": "6.0.10",
"@types/chromecast-caf-sender": "1.0.5", "@types/chromecast-caf-sender": "1.0.6",
"@types/esprima": "4.0.3", "@types/esprima": "4.0.3",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.0", "@types/html-minifier-terser": "7.0.0",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.6",
"@types/leaflet": "1.9.3", "@types/leaflet": "1.9.4",
"@types/leaflet-draw": "1.0.7", "@types/leaflet-draw": "1.0.8",
"@types/luxon": "3.3.1", "@types/luxon": "3.3.2",
"@types/mocha": "10.0.1", "@types/mocha": "10.0.1",
"@types/qrcode": "1.5.1", "@types/qrcode": "1.5.2",
"@types/serve-handler": "6.1.1", "@types/serve-handler": "6.1.1",
"@types/sortablejs": "1.15.1", "@types/sortablejs": "1.15.2",
"@types/tar": "6.1.5", "@types/tar": "6.1.6",
"@types/ua-parser-js": "0.7.36", "@types/ua-parser-js": "0.7.37",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.4.1", "@typescript-eslint/eslint-plugin": "6.7.0",
"@typescript-eslint/parser": "6.4.1", "@typescript-eslint/parser": "6.7.0",
"@web/dev-server": "0.1.38", "@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1", "@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.8", "chai": "4.3.8",
"del": "7.0.0", "del": "7.1.0",
"eslint": "8.47.0", "eslint": "8.49.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0", "eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "9.0.0", "eslint-config-prettier": "9.0.0",
@@ -208,7 +208,7 @@
"esprima": "4.0.1", "esprima": "4.0.1",
"fancy-log": "2.0.0", "fancy-log": "2.0.0",
"fs-extra": "11.1.1", "fs-extra": "11.1.1",
"glob": "10.3.3", "glob": "10.3.4",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-flatmap": "1.0.2", "gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8", "gulp-json-transform": "0.4.8",
@@ -228,16 +228,16 @@
"object-hash": "3.0.0", "object-hash": "3.0.0",
"open": "9.1.0", "open": "9.1.0",
"pinst": "3.0.0", "pinst": "3.0.0",
"prettier": "3.0.2", "prettier": "3.0.3",
"rollup": "2.79.1", "rollup": "2.79.1",
"rollup-plugin-string": "3.0.0", "rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.9.2", "rollup-plugin-visualizer": "5.9.2",
"serve-handler": "6.1.5", "serve-handler": "6.1.5",
"sinon": "15.2.0", "sinon": "16.0.0",
"source-map-url": "0.4.1", "source-map-url": "0.4.1",
"systemjs": "6.14.2", "systemjs": "6.14.2",
"tar": "6.1.15", "tar": "6.2.0",
"terser-webpack-plugin": "5.3.9", "terser-webpack-plugin": "5.3.9",
"ts-lit-plugin": "2.0.0-pre.1", "ts-lit-plugin": "2.0.0-pre.1",
"typescript": "5.2.2", "typescript": "5.2.2",

View File

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

View File

@@ -8,40 +8,4 @@ set -eu -o pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
if [ -z "${LOKALISE_TOKEN-}" ] && [ ! -f .lokalise_token ] ; then ./node_modules/.bin/gulp download-translations
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

View File

@@ -35,20 +35,47 @@ export class HaPasswordManagerPolyfill extends LitElement {
super.connectedCallback(); super.connectedCallback();
this._styleElement = document.createElement("style"); this._styleElement = document.createElement("style");
this._styleElement.textContent = css` this._styleElement.textContent = css`
/* Polyfill form is sized and vertically aligned with true form, then positioned offscreen
rather than hiding so it does not create a new stacking context */
.password-manager-polyfill { .password-manager-polyfill {
position: absolute; position: absolute;
opacity: 0; box-sizing: border-box;
z-index: -1;
} }
.password-manager-polyfill input { /* Excluding our wrapper, move any children back on screen, including anything injected that might not already be positioned */
.password-manager-polyfill > *:not(.wrapper),
.password-manager-polyfill > .wrapper > * {
position: relative;
left: 10000px;
}
/* Size and hide our polyfill fields */
.password-manager-polyfill .underneath {
display: block;
box-sizing: border-box;
width: 100%; width: 100%;
height: 62px; padding: 0 16px;
padding: 0;
border: 0; border: 0;
z-index: -1;
height: 21px;
/* Transparency is only needed to hide during paint or in case of misalignment,
but LastPass will fail if it's 0, so we use 1% */
opacity: 0.01;
} }
.password-manager-polyfill input[type="submit"] { .password-manager-polyfill input.underneath {
width: 0; height: 28px;
height: 0; margin-bottom: 30.5px;
}
/* Button position is not important, but size should not be zero */
.password-manager-polyfill > input.underneath[type="submit"] {
width: 1px;
height: 1px;
margin: 0 auto;
overflow: hidden;
}
/* Ensure injected elements will be on top */
.password-manager-polyfill > *:not(.underneath, .wrapper),
.password-manager-polyfill > .wrapper > *:not(.underneath) {
isolation: isolate;
z-index: auto;
} }
`.toString(); `.toString();
document.head.append(this._styleElement); document.head.append(this._styleElement);
@@ -77,16 +104,25 @@ export class HaPasswordManagerPolyfill extends LitElement {
class="password-manager-polyfill" class="password-manager-polyfill"
style=${styleMap({ style=${styleMap({
top: `${this.boundingRect?.y || 148}px`, top: `${this.boundingRect?.y || 148}px`,
left: `calc(50% - ${(this.boundingRect?.width || 360) / 2}px)`, left: `calc(50% - ${
(this.boundingRect?.width || 360) / 2
}px - 10000px)`,
width: `${this.boundingRect?.width || 360}px`, width: `${this.boundingRect?.width || 360}px`,
})} })}
aria-hidden="true" action="/auth"
method="post"
@submit=${this._handleSubmit} @submit=${this._handleSubmit}
> >
${autocompleteLoginFields(this.step.data_schema).map((input) => ${autocompleteLoginFields(this.step.data_schema).map((input) =>
this.render_input(input) this.render_input(input)
)} )}
<input type="submit" /> <input
type="submit"
value="Login"
class="underneath"
tabindex="-2"
aria-hidden="true"
/>
</form> </form>
`; `;
} }
@@ -99,25 +135,35 @@ export class HaPasswordManagerPolyfill extends LitElement {
return ""; return "";
} }
return html` return html`
<input <!-- Label is a sibling so it can be stacked underneath without affecting injections adjacent to input (e.g. LastPass) -->
tabindex="-1" <label for=${schema.name} class="underneath" aria-hidden="true">
.id=${schema.name} ${schema.name}
.name=${schema.name} </label>
.type=${inputType} <!-- LastPass fails if the input is hidden directly, so we trick it and hide a wrapper instead -->
.value=${this.stepData[schema.name] || ""} <div class="wrapper" aria-hidden="true">
.autocomplete=${schema.autocomplete} <!-- LastPass fails with tabindex of -1, so we trick with -2 -->
@input=${this._valueChanged} <input
/> class="underneath"
tabindex="-2"
.id=${schema.name}
.name=${schema.name}
.type=${inputType}
.value=${this.stepData[schema.name] || ""}
.autocomplete=${schema.autocomplete}
@input=${this._valueChanged}
@change=${this._valueChanged}
/>
</div>
`; `;
} }
private _handleSubmit(ev: Event) { private _handleSubmit(ev: SubmitEvent) {
ev.preventDefault(); ev.preventDefault();
fireEvent(this, "form-submitted"); fireEvent(this, "form-submitted");
} }
private _valueChanged(ev: Event) { private _valueChanged(ev: Event) {
const target = ev.target! as HTMLInputElement; const target = ev.target as HTMLInputElement;
this.stepData = { ...this.stepData, [target.id]: target.value }; this.stepData = { ...this.stepData, [target.id]: target.value };
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: this.stepData, value: this.stepData,

View File

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

View File

@@ -22,14 +22,7 @@ export type LocalizeKeys =
| `ui.dialogs.unhealthy.reason.${string}` | `ui.dialogs.unhealthy.reason.${string}`
| `ui.dialogs.unsupported.reason.${string}` | `ui.dialogs.unsupported.reason.${string}`
| `ui.panel.config.${string}.${"caption" | "description"}` | `ui.panel.config.${string}.${"caption" | "description"}`
| `ui.panel.config.automation.${string}`
| `ui.panel.config.dashboard.${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.zha.${string}`
| `ui.panel.config.zwave_js.${string}` | `ui.panel.config.zwave_js.${string}`
| `ui.panel.lovelace.card.${string}` | `ui.panel.lovelace.card.${string}`

View File

@@ -15,13 +15,20 @@ import { HomeAssistant } from "../../types";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; 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; top: string;
left: string; left: string;
} }
@customElement("ha-chart-base") @customElement("ha-chart-base")
export default class HaChartBase extends LitElement { export class HaChartBase extends LitElement {
public chart?: Chart; public chart?: Chart;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -45,14 +52,6 @@ export default class HaChartBase extends LitElement {
@state() private _hiddenDatasets: Set<number> = new Set(); @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() { public disconnectedCallback() {
this._releaseCanvas(); this._releaseCanvas();
super.disconnectedCallback(); 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() { protected firstUpdated() {
this._setupChart(); this._setupChart();
this.data.datasets.forEach((dataset, index) => { this.data.datasets.forEach((dataset, index) => {
@@ -80,14 +109,11 @@ export default class HaChartBase extends LitElement {
if (!this.hasUpdated || !this.chart) { if (!this.hasUpdated || !this.chart) {
return; return;
} }
if (changedProps.has("plugins")) { if (changedProps.has("plugins") || changedProps.has("chartType")) {
this.chart.destroy(); this.chart.destroy();
this._setupChart(); this._setupChart();
return; return;
} }
if (changedProps.has("chartType")) {
this.chart.config.type = this.chartType;
}
if (changedProps.has("data")) { if (changedProps.has("data")) {
if (this._hiddenDatasets.size) { if (this._hiddenDatasets.size) {
this.data.datasets.forEach((dataset, index) => { this.data.datasets.forEach((dataset, index) => {
@@ -131,55 +157,70 @@ export default class HaChartBase extends LitElement {
</div>` </div>`
: ""} : ""}
<div <div
class="chartContainer" class="animationContainer"
style=${styleMap({ style=${styleMap({
height: `${this.height ?? this._chartHeight}px`, height: `${this.height || this._chartHeight || 0}px`,
overflow: this._chartHeight ? "initial" : "hidden", 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> <div
${this._tooltip class="chartContainer"
? html`<div style=${styleMap({
class="chartTooltip ${classMap({ [this._tooltip.yAlign]: true })}" height: `${
style=${styleMap({ this.height ?? this._chartHeight ?? this.clientWidth / 2
top: this._tooltip.top, }px`,
left: this._tooltip.left, "padding-left": `${
})} computeRTL(this.hass) ? 0 : this.paddingYAxis
> }px`,
<div class="title">${this._tooltip.title}</div> "padding-right": `${
${this._tooltip.beforeBody computeRTL(this.hass) ? this.paddingYAxis : 0
? html`<div class="beforeBody"> }px`,
${this._tooltip.beforeBody} })}
</div>` >
: ""} <canvas></canvas>
<div> ${this._tooltip
<ul> ? html`<div
${this._tooltip.body.map( class="chartTooltip ${classMap({
(item, i) => [this._tooltip.yAlign]: true,
html`<li> })}"
<div style=${styleMap({
class="bullet" top: this._tooltip.top,
style=${styleMap({ left: this._tooltip.left,
backgroundColor: this._tooltip!.labelColors[i] })}
.backgroundColor as string, >
borderColor: this._tooltip!.labelColors[i] <div class="title">${this._tooltip.title}</div>
.borderColor as string, ${this._tooltip.beforeBody
})} ? html`<div class="beforeBody">
></div> ${this._tooltip.beforeBody}
${item.lines.join("\n")} </div>`
</li>` : ""}
)} <div>
</ul> <ul>
</div> ${this._tooltip.body.map(
${this._tooltip.footer.length (item, i) =>
? html`<div class="footer"> html`<li>
${this._tooltip.footer.map((item) => html`${item}<br />`)} <div
</div>` class="bullet"
: ""} style=${styleMap({
</div>` 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> </div>
`; `;
} }
@@ -213,6 +254,7 @@ export default class HaChartBase extends LitElement {
private _createOptions() { private _createOptions() {
return { return {
maintainAspectRatio: false,
...this.options, ...this.options,
plugins: { plugins: {
...this.options?.plugins, ...this.options?.plugins,
@@ -233,10 +275,10 @@ export default class HaChartBase extends LitElement {
return [ return [
...(this.plugins || []), ...(this.plugins || []),
{ {
id: "afterRenderHook", id: "resizeHook",
afterRender: (chart) => { resize: (chart) => {
const change = chart.height - (this._chartHeight ?? 0); 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 // hysteresis to prevent infinite render loops
this._chartHeight = chart.height; this._chartHeight = chart.height;
} }
@@ -288,21 +330,13 @@ export default class HaChartBase extends LitElement {
}; };
} }
public updateChart = ( private _releaseCanvas() {
mode: // release the canvas memory to prevent
| "resize" // safari from running out of memory.
| "reset"
| "none"
| "hide"
| "show"
| "normal"
| "active"
| undefined
): void => {
if (this.chart) { if (this.chart) {
this.chart.update(mode); this.chart.destroy();
} }
}; }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
@@ -310,7 +344,7 @@ export default class HaChartBase extends LitElement {
display: block; display: block;
position: var(--chart-base-position, relative); position: var(--chart-base-position, relative);
} }
.chartContainer { .animationContainer {
overflow: hidden; overflow: hidden;
height: 0; height: 0;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1); transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);

View File

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

View File

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

View File

@@ -6,7 +6,13 @@ import {
nothing, nothing,
PropertyValues, PropertyValues,
} from "lit"; } 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 { isComponentLoaded } from "../../common/config/is_component_loaded";
import { restoreScroll } from "../../common/decorators/restore-scroll"; import { restoreScroll } from "../../common/decorators/restore-scroll";
import { import {
@@ -18,6 +24,9 @@ import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "./state-history-chart-line"; import "./state-history-chart-line";
import "./state-history-chart-timeline"; 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 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 // @ts-ignore
@restoreScroll(".container") private _savedScrollPos?: number; @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() { protected render() {
if (!isComponentLoaded(this.hass, "history")) { if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info"> return html`<div class="info">

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,95 @@ import { BarController, BarElement } from "chart.js";
import { TimeLineData } from "./const"; import { TimeLineData } from "./const";
import { TextBarProps } from "./textbar-element"; 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) { function parseValue(entry, item, vScale, i) {
const startValue = vScale.parse(entry.start, i); const startValue = vScale.parse(entry.start, i);
const endValue = vScale.parse(entry.end, i); const endValue = vScale.parse(entry.end, i);
@@ -97,7 +186,7 @@ export class TimelineController extends BarController {
bars: BarElement[], bars: BarElement[],
start: number, start: number,
count: 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 vScale = this._cachedMeta.vScale!;
const iScale = this._cachedMeta.iScale!; const iScale = this._cachedMeta.iScale!;
@@ -114,15 +203,15 @@ export class TimelineController extends BarController {
for (let index = start; index < start + count; index++) { for (let index = start; index < start + count; index++) {
const data = dataset.data[index] as TimeLineData; const data = dataset.data[index] as TimeLineData;
// @ts-ignore
const y = vScale.getPixelForValue(this.index); const y = vScale.getPixelForValue(this.index);
// @ts-ignore
const xStart = iScale.getPixelForValue(data.start.getTime()); const xStart = iScale.getPixelForValue(data.start.getTime());
// @ts-ignore
const xEnd = iScale.getPixelForValue(data.end.getTime()); const xEnd = iScale.getPixelForValue(data.end.getTime());
const width = xEnd - xStart; const width = xEnd - xStart;
const parsed = this.getParsed(index);
const stack = (parsed._stacks || {})[vScale.axis];
const height = 10; const height = 10;
const properties: TextBarProps = { const properties: TextBarProps = {
@@ -145,7 +234,10 @@ export class TimelineController extends BarController {
backgroundColor: data.color, 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); this.updateElement(bars[index], index, properties as any, mode);
} }
} }

View File

@@ -74,7 +74,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
title: TemplateResult | string; title: TemplateResult | string;
label?: TemplateResult | string; label?: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex"; type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
template?: (data: any, row: T) => TemplateResult | string | typeof nothing; template?: (row: T) => TemplateResult | string | typeof nothing;
width?: string; width?: string;
maxWidth?: string; maxWidth?: string;
grows?: boolean; grows?: boolean;
@@ -431,7 +431,7 @@ export class HaDataTable extends LitElement {
}) })
: ""} : ""}
> >
${column.template ? column.template(row[key], row) : row[key]} ${column.template ? column.template(row) : row[key]}
</div> </div>
`; `;
})} })}
@@ -458,7 +458,8 @@ export class HaDataTable extends LitElement {
filteredData, filteredData,
this._sortColumns[this._sortColumn], this._sortColumns[this._sortColumn],
this._sortDirection, this._sortDirection,
this._sortColumn this._sortColumn,
this.hass.locale.language
) )
: filteredData; : filteredData;

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { arrayLiteralIncludes } from "../../common/array/literal-includes"; import { arrayLiteralIncludes } from "../../common/array/literal-includes";
import secondsToDuration from "../../common/datetime/seconds_to_duration"; 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 { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states"; import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
@@ -192,13 +191,7 @@ export class HaStateLabelBadge extends LitElement {
this.hass!.locale, this.hass!.locale,
getNumberFormatOptions(entityState, entry) getNumberFormatOptions(entityState, entry)
) )
: computeStateDisplay( : this.hass!.formatEntityState(entityState);
this.hass!.localize,
entityState,
this.hass!.locale,
this.hass!.config,
this.hass!.entities
);
} }
} }

View File

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

View File

@@ -1,9 +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 { customElement, property } from "lit/decorators";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { CLIMATE_PRESET_NONE, ClimateEntity } from "../data/climate"; import { CLIMATE_PRESET_NONE, ClimateEntity } from "../data/climate";
import { isUnavailableState } from "../data/entity"; import { isUnavailableState, OFF } from "../data/entity";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@customElement("ha-climate-state") @customElement("ha-climate-state")
@@ -22,26 +27,24 @@ class HaClimateState extends LitElement {
${this.stateObj.attributes.preset_mode && ${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`- ? html`-
${computeAttributeValueDisplay( ${this.hass.formatEntityAttributeValue(
this.hass.localize,
this.stateObj, this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"preset_mode" "preset_mode"
)}` )}`
: ""} : nothing}
</span> </span>
<div class="unit">${this._computeTarget()}</div>` <div class="unit">${this._computeTarget()}</div>`
: this._localizeState()} : this._localizeState()}
</div> </div>
${currentStatus && !isUnavailableState(this.stateObj.state) ${currentStatus && !isUnavailableState(this.stateObj.state)
? html`<div class="current"> ? html`
${this.hass.localize("ui.card.climate.currently")}: <div class="current">
<div class="unit">${currentStatus}</div> ${this.hass.localize("ui.card.climate.currently")}:
</div>` <div class="unit">${currentStatus}</div>
: ""}`; </div>
`
: nothing}`;
} }
private _computeCurrentStatus(): string | undefined { private _computeCurrentStatus(): string | undefined {
@@ -125,24 +128,17 @@ class HaClimateState extends LitElement {
return this.hass.localize(`state.default.${this.stateObj.state}`); return this.hass.localize(`state.default.${this.stateObj.state}`);
} }
const stateString = computeStateDisplay( const stateString = this.hass.formatEntityState(this.stateObj);
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
);
return this.stateObj.attributes.hvac_action if (this.stateObj.attributes.hvac_action && this.stateObj.state !== OFF) {
? `${computeAttributeValueDisplay( const actionString = this.hass.formatEntityAttributeValue(
this.hass.localize, this.stateObj,
this.stateObj, "hvac_action"
this.hass.locale, );
this.hass.config, return `${actionString} (${stateString})`;
this.hass.entities, }
"hvac_action"
)} (${stateString})` return stateString;
: stateString;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -244,7 +244,6 @@ export class HaComboBox extends LitElement {
); );
if (overlay) { if (overlay) {
overlay.setAttribute("required-vertical-space", "0");
this._removeInert(overlay); this._removeInert(overlay);
} }
this._observeBody(); this._observeBody();
@@ -331,7 +330,7 @@ export class HaComboBox extends LitElement {
} }
vaadin-combo-box-light { vaadin-combo-box-light {
position: relative; position: relative;
--vaadin-combo-box-overlay-max-height: calc(45vh); --vaadin-combo-box-overlay-max-height: calc(45vh - 56px);
} }
ha-textfield { ha-textfield {
width: 100%; width: 100%;

View File

@@ -81,6 +81,7 @@ export class HaControlNumberButton extends LitElement {
} }
_handleKeyDown(e: KeyboardEvent) { _handleKeyDown(e: KeyboardEvent) {
if (this.disabled) return;
if (!A11Y_KEY_CODES.has(e.code)) return; if (!A11Y_KEY_CODES.has(e.code)) return;
e.preventDefault(); e.preventDefault();
switch (e.code) { switch (e.code) {
@@ -116,7 +117,7 @@ export class HaControlNumberButton extends LitElement {
const displayedValue = const displayedValue =
this.value != null this.value != null
? formatNumber(this.value, this.locale, this.formatOptions) ? formatNumber(this.value, this.locale, this.formatOptions)
: "-"; : "";
return html` return html`
<div class="container"> <div class="container">
@@ -124,12 +125,12 @@ export class HaControlNumberButton extends LitElement {
id="input" id="input"
class="value" class="value"
role="number-button" role="number-button"
tabindex="0" .tabIndex=${this.disabled ? "-1" : "0"}
aria-valuenow=${this.value} aria-valuenow=${this.value}
aria-valuemin=${this.min} aria-valuemin=${this.min}
aria-valuemax=${this.max} aria-valuemax=${this.max}
aria-label=${ifDefined(this.label)} aria-label=${ifDefined(this.label)}
.disabled=${this.disabled} ?disabled=${this.disabled}
@keydown=${this._handleKeyDown} @keydown=${this._handleKeyDown}
> >
${displayedValue} ${displayedValue}
@@ -240,6 +241,7 @@ export class HaControlNumberButton extends LitElement {
.button[disabled] { .button[disabled] {
opacity: 0.4; opacity: 0.4;
pointer-events: none; pointer-events: none;
cursor: not-allowed;
} }
.button.minus { .button.minus {
left: 0; left: 0;

View File

@@ -1,10 +1,12 @@
import { Ripple } from "@material/mwc-ripple"; import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers"; import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { SelectBase } from "@material/mwc-select/mwc-select-base"; import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { mdiMenuDown } from "@mdi/js";
import { css, html, nothing } from "lit"; import { css, html, nothing } from "lit";
import { import {
customElement, customElement,
eventOptions, eventOptions,
property,
query, query,
queryAsync, queryAsync,
state, state,
@@ -24,6 +26,12 @@ export class HaControlSelectMenu extends SelectBase {
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null; @query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
@property({ type: Boolean, attribute: "show-arrow" })
public showArrow?: boolean;
@property({ type: Boolean, attribute: "hide-label" })
public hideLabel?: boolean;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>; @queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false; @state() private _shouldRenderRipple = false;
@@ -36,7 +44,9 @@ export class HaControlSelectMenu extends SelectBase {
"select-no-value": !this.selectedText, "select-no-value": !this.selectedText,
}; };
const labelledby = this.label ? "label" : undefined; const labelledby = this.label && !this.hideLabel ? "label" : undefined;
const labelAttribute =
this.label && this.hideLabel ? this.label : undefined;
return html` return html`
<div class="select ${classMap(classes)}"> <div class="select ${classMap(classes)}">
@@ -57,6 +67,7 @@ export class HaControlSelectMenu extends SelectBase {
aria-invalid=${!this.isUiValid} aria-invalid=${!this.isUiValid}
aria-haspopup="listbox" aria-haspopup="listbox"
aria-labelledby=${ifDefined(labelledby)} aria-labelledby=${ifDefined(labelledby)}
aria-label=${ifDefined(labelAttribute)}
aria-required=${this.required} aria-required=${this.required}
@click=${this.onClick} @click=${this.onClick}
@focus=${this.onFocus} @focus=${this.onFocus}
@@ -72,11 +83,14 @@ export class HaControlSelectMenu extends SelectBase {
> >
${this.renderIcon()} ${this.renderIcon()}
<div class="content"> <div class="content">
<p id="label" class="label">${this.label}</p> ${this.hideLabel
? nothing
: html`<p id="label" class="label">${this.label}</p>`}
${this.selectedText ${this.selectedText
? html`<p class="value">${this.selectedText}</p>` ? html`<p class="value">${this.selectedText}</p>`
: nothing} : nothing}
</div> </div>
${this.renderArrow()}
${this._shouldRenderRipple && !this.disabled ${this._shouldRenderRipple && !this.disabled
? html` <mwc-ripple></mwc-ripple> ` ? html` <mwc-ripple></mwc-ripple> `
: nothing} : nothing}
@@ -86,13 +100,29 @@ export class HaControlSelectMenu extends SelectBase {
`; `;
} }
private renderArrow() {
if (!this.showArrow) return nothing;
return html`
<div class="icon">
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
</div>
`;
}
private renderIcon() { private renderIcon() {
const index = this.mdcFoundation?.getSelectedIndex(); const index = this.mdcFoundation?.getSelectedIndex();
const items = this.menuElement?.items ?? []; const items = this.menuElement?.items ?? [];
const item = index != null ? items[index] : undefined; const item = index != null ? items[index] : undefined;
const icon = const defaultIcon = this.querySelector("[slot='icon']");
item?.querySelector("[slot='graphic']") ?? const icon = (item?.querySelector("[slot='graphic']") ?? null) as
(null as HaSvgIcon | HaIcon | null); | HaSvgIcon
| HaIcon
| null;
if (!defaultIcon && !icon) {
return null;
}
return html` return html`
<div class="icon"> <div class="icon">
@@ -171,14 +201,18 @@ export class HaControlSelectMenu extends SelectBase {
--control-select-menu-background-color: var(--disabled-color); --control-select-menu-background-color: var(--disabled-color);
--control-select-menu-background-opacity: 0.2; --control-select-menu-background-opacity: 0.2;
--control-select-menu-border-radius: 14px; --control-select-menu-border-radius: 14px;
--control-select-menu-height: 48px;
--control-select-menu-padding: 6px 10px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
font-size: 14px;
line-height: 1.4;
width: auto; width: auto;
color: var(--primary-text-color); color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
.select-anchor { .select-anchor {
height: 48px; height: var(--control-select-menu-height);
padding: 6px 10px; padding: var(--control-select-menu-padding);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
@@ -193,15 +227,12 @@ export class HaControlSelectMenu extends SelectBase {
--mdc-ripple-color: var(--control-select-menu-background-color); --mdc-ripple-color: var(--control-select-menu-background-color);
/* For safari border-radius overflow */ /* For safari border-radius overflow */
z-index: 0; z-index: 0;
font-size: inherit;
transition: color 180ms ease-in-out; transition: color 180ms ease-in-out;
gap: 10px; gap: 10px;
width: 100%; width: 100%;
user-select: none; user-select: none;
font-size: 14px;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px;
letter-spacing: 0.25px; letter-spacing: 0.25px;
} }
.content { .content {
@@ -223,8 +254,7 @@ export class HaControlSelectMenu extends SelectBase {
} }
.label { .label {
font-size: 12px; font-size: 0.85em;
line-height: 16px;
letter-spacing: 0.4px; letter-spacing: 0.4px;
} }

View File

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

View File

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

View File

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

View File

@@ -68,6 +68,7 @@ export class HaFormString extends LitElement implements HaFormElement {
: this.schema.description?.suffix} : this.schema.description?.suffix}
.validationMessage=${this.schema.required ? "Required" : undefined} .validationMessage=${this.schema.required ? "Required" : undefined}
@input=${this._valueChanged} @input=${this._valueChanged}
@change=${this._valueChanged}
></ha-textfield> ></ha-textfield>
${isPassword ${isPassword
? html`<ha-icon-button ? 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 { rgbw2rgb, rgbww2rgb } from "../common/color/convert-light-color";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"cursor-moved": { value?: any };
}
}
function xy2polar(x: number, y: number) { function xy2polar(x: number, y: number) {
const r = Math.sqrt(x * x + y * y); const r = Math.sqrt(x * x + y * y);
const phi = Math.atan2(y, x); const phi = Math.atan2(y, x);

View File

@@ -1,7 +1,5 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { isUnavailableState, OFF } from "../data/entity"; import { isUnavailableState, OFF } from "../data/entity";
import { HumidifierEntity } from "../data/humidifier"; import { HumidifierEntity } from "../data/humidifier";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@@ -21,12 +19,8 @@ class HaHumidifierState extends LitElement {
${this._localizeState()} ${this._localizeState()}
${this.stateObj.attributes.mode ${this.stateObj.attributes.mode
? html`- ? html`-
${computeAttributeValueDisplay( ${this.hass.formatEntityAttributeValue(
this.hass.localize,
this.stateObj, this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"mode" "mode"
)}` )}`
: ""} : ""}
@@ -78,24 +72,17 @@ class HaHumidifierState extends LitElement {
return this.hass.localize(`state.default.${this.stateObj.state}`); return this.hass.localize(`state.default.${this.stateObj.state}`);
} }
const stateString = computeStateDisplay( const stateString = this.hass.formatEntityState(this.stateObj);
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
);
return this.stateObj.attributes.action && this.stateObj.state !== OFF if (this.stateObj.attributes.action && this.stateObj.state !== OFF) {
? `${computeAttributeValueDisplay( const actionString = this.hass.formatEntityAttributeValue(
this.hass.localize, this.stateObj,
this.stateObj, "action"
this.hass.locale, );
this.hass.config, return `${actionString} (${stateString})`;
this.hass.entities, }
"action"
)} (${stateString})` return stateString;
: stateString;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -1,5 +1,5 @@
import { mdiImagePlus } from "@mdi/js"; 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 { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { createImage, generateImageThumbnailUrl } from "../data/image_upload"; import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
@@ -9,6 +9,7 @@ import {
showImageCropperDialog, showImageCropperDialog,
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog"; } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-button";
import "./ha-circular-progress"; import "./ha-circular-progress";
import "./ha-file-upload"; import "./ha-file-upload";
@@ -20,6 +21,12 @@ export class HaPictureUpload extends LitElement {
@property() public label?: string; @property() public label?: string;
@property() public secondary?: string;
@property() public supports?: string;
@property() public currentImageAltText?: string;
@property({ type: Boolean }) public crop = false; @property({ type: Boolean }) public crop = false;
@property({ attribute: false }) public cropOptions?: CropOptions; @property({ attribute: false }) public cropOptions?: CropOptions;
@@ -29,19 +36,44 @@ export class HaPictureUpload extends LitElement {
@state() private _uploading = false; @state() private _uploading = false;
public render(): TemplateResult { public render(): TemplateResult {
return html` if (!this.value) {
<ha-file-upload return html`
.hass=${this.hass} <ha-file-upload
.icon=${mdiImagePlus} .hass=${this.hass}
.label=${this.label || .icon=${mdiImagePlus}
this.hass.localize("ui.components.picture-upload.label")} .label=${this.label ||
.uploading=${this._uploading} this.hass.localize("ui.components.picture-upload.label")}
.value=${this.value ? html`<img .src=${this.value} />` : ""} .secondary=${this.secondary}
@file-picked=${this._handleFilePicked} .supports=${this.supports ||
@change=${this._handleFileCleared} this.hass.localize("ui.components.picture-upload.supported_formats")}
accept="image/png, image/jpeg, image/gif" .uploading=${this._uploading}
></ha-file-upload> @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) { private async _handleFilePicked(ev) {
@@ -100,6 +132,35 @@ export class HaPictureUpload extends LitElement {
this._uploading = false; 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 { declare global {

View File

@@ -37,9 +37,12 @@ export class HaFileSelector extends LitElement {
.label=${this.label} .label=${this.label}
.required=${this.required} .required=${this.required}
.disabled=${this.disabled} .disabled=${this.disabled}
.helper=${this.helper} .supports=${this.helper}
.uploading=${this._busy} .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} @file-picked=${this._uploadFile}
@change=${this._removeFile} @change=${this._removeFile}
></ha-file-upload> ></ha-file-upload>

View File

@@ -1,440 +0,0 @@
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
import { LitElement, PropertyValues, css, html, 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 {
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
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 = DEFAULT_MIN_KELVIN;
@property({ type: Number })
public max = DEFAULT_MAX_KELVIN;
@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"; import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/number/format_number"; import { formatNumber } from "../common/number/format_number";
import LocalizeMixin from "../mixins/localize-mixin"; import LocalizeMixin from "../mixins/localize-mixin";
@@ -84,12 +83,7 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
} }
_localizeState(stateObj) { _localizeState(stateObj) {
return computeStateDisplay( return this.hass.formatEntityState(stateObj);
this.hass.localize,
stateObj,
this.hass.locale,
this.hass.entities
);
} }
} }
customElements.define("ha-water_heater-state", HaWaterHeaterState); customElements.define("ha-water_heater-state", HaWaterHeaterState);

View File

@@ -238,11 +238,13 @@ export interface ZoneCondition extends BaseCondition {
zone: string; zone: string;
} }
type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
export interface TimeCondition extends BaseCondition { export interface TimeCondition extends BaseCondition {
condition: "time"; condition: "time";
after?: string; after?: string;
before?: string; before?: string;
weekday?: string | string[]; weekday?: Weekday | Weekday[];
} }
export interface TemplateCondition extends BaseCondition { export interface TemplateCondition extends BaseCondition {

View File

@@ -6,11 +6,7 @@ import {
formatTimeWithSeconds, formatTimeWithSeconds,
} from "../common/datetime/format_time"; } from "../common/datetime/format_time";
import secondsToDuration from "../common/datetime/seconds_to_duration"; import secondsToDuration from "../common/datetime/seconds_to_duration";
import { import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
computeAttributeNameDisplay,
computeAttributeValueDisplay,
} from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import "../resources/intl-polyfill"; import "../resources/intl-polyfill";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@@ -235,23 +231,14 @@ const tryDescribeTrigger = (
for (const state of trigger.from.values()) { for (const state of trigger.from.values()) {
from.push( from.push(
trigger.attribute trigger.attribute
? computeAttributeValueDisplay( ? hass
hass.localize, .formatEntityAttributeValue(
stateObj, stateObj,
hass.locale, trigger.attribute,
hass.config, state
hass.entities, )
trigger.attribute, .toString()
state : hass.formatEntityState(stateObj, state)
).toString()
: computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
state
)
); );
} }
if (from.length !== 0) { if (from.length !== 0) {
@@ -261,23 +248,16 @@ const tryDescribeTrigger = (
} else { } else {
base += ` from ${ base += ` from ${
trigger.attribute trigger.attribute
? computeAttributeValueDisplay( ? hass
hass.localize, .formatEntityAttributeValue(
stateObj, stateObj,
hass.locale, trigger.attribute,
hass.config, trigger.from
hass.entities, )
trigger.attribute, .toString()
trigger.from : hass
).toString() .formatEntityState(stateObj, trigger.from.toString())
: computeStateDisplay( .toString()
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
trigger.from.toString()
).toString()
}`; }`;
} }
} }
@@ -292,23 +272,14 @@ const tryDescribeTrigger = (
for (const state of trigger.to.values()) { for (const state of trigger.to.values()) {
to.push( to.push(
trigger.attribute trigger.attribute
? computeAttributeValueDisplay( ? hass
hass.localize, .formatEntityAttributeValue(
stateObj, stateObj,
hass.locale, trigger.attribute,
hass.config, state
hass.entities, )
trigger.attribute, .toString()
state : hass.formatEntityState(stateObj, state).toString()
).toString()
: computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
state
).toString()
); );
} }
if (to.length !== 0) { if (to.length !== 0) {
@@ -318,23 +289,14 @@ const tryDescribeTrigger = (
} else { } else {
base += ` to ${ base += ` to ${
trigger.attribute trigger.attribute
? computeAttributeValueDisplay( ? hass
hass.localize, .formatEntityAttributeValue(
stateObj, stateObj,
hass.locale, trigger.attribute,
hass.config, trigger.to
hass.entities, )
trigger.attribute, .toString()
trigger.to : hass.formatEntityState(stateObj, trigger.to.toString())
).toString()
: computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
trigger.to.toString()
)
}`; }`;
} }
} }
@@ -822,45 +784,27 @@ const tryDescribeCondition = (
for (const state of condition.state.values()) { for (const state of condition.state.values()) {
states.push( states.push(
condition.attribute condition.attribute
? computeAttributeValueDisplay( ? hass
hass.localize, .formatEntityAttributeValue(
stateObj, stateObj,
hass.locale, condition.attribute,
hass.config, state
hass.entities, )
condition.attribute, .toString()
state : hass.formatEntityState(stateObj, state)
).toString()
: computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
state
)
); );
} }
} else if (condition.state !== "") { } else if (condition.state !== "") {
states.push( states.push(
condition.attribute condition.attribute
? computeAttributeValueDisplay( ? hass
hass.localize, .formatEntityAttributeValue(
stateObj, stateObj,
hass.locale, condition.attribute,
hass.config, condition.state
hass.entities, )
condition.attribute, .toString()
condition.state : hass.formatEntityState(stateObj, condition.state.toString())
).toString()
: computeStateDisplay(
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
condition.state.toString()
)
); );
} }

View File

@@ -1,7 +1,5 @@
import { EntityFilter } from "../common/entity/entity_filter"; import { EntityFilter } from "../common/entity/entity_filter";
import { PlaceholderContainer } from "../panels/config/automation/thingtalk/dialog-thingtalk";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { AutomationConfig } from "./automation";
interface CloudStatusNotLoggedIn { interface CloudStatusNotLoggedIn {
logged_in: false; logged_in: false;
@@ -13,6 +11,7 @@ export interface CertificateInformation {
common_name: string; common_name: string;
expire_date: string; expire_date: string;
fingerprint: string; fingerprint: string;
alternative_names: string[];
} }
export interface CloudPreferences { export interface CloudPreferences {
@@ -66,11 +65,6 @@ export interface CloudWebhook {
managed?: boolean; managed?: boolean;
} }
export interface ThingTalkConversion {
config: Partial<AutomationConfig>;
placeholders: PlaceholderContainer;
}
export const cloudLogin = ( export const cloudLogin = (
hass: HomeAssistant, hass: HomeAssistant,
email: string, email: string,
@@ -136,9 +130,6 @@ export const disconnectCloudRemote = (hass: HomeAssistant) =>
export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) => export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) =>
hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" }); hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" });
export const convertThingTalk = (hass: HomeAssistant, query: string) =>
hass.callWS<ThingTalkConversion>({ type: "cloud/thingtalk/convert", query });
export const updateCloudPref = ( export const updateCloudPref = (
hass: HomeAssistant, hass: HomeAssistant,
prefs: { prefs: {

View File

@@ -1,46 +1,25 @@
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise"; import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
interface EntitySourceConfigEntry { interface EntitySource {
source: "config_entry";
domain: string; domain: string;
custom_component: boolean;
config_entry: string;
} }
interface EntitySourcePlatformConfig { export type EntitySources = Record<string, EntitySource>;
source: "platform_config";
domain: string;
custom_component: boolean;
}
export type EntitySources = Record< const fetchEntitySources = (hass: HomeAssistant): Promise<EntitySources> =>
string, hass.callWS({ type: "entity/source" });
EntitySourceConfigEntry | EntitySourcePlatformConfig
>;
const fetchEntitySources = (
hass: HomeAssistant,
entity_id?: string
): Promise<EntitySources> =>
hass.callWS({
type: "entity/source",
entity_id,
});
export const fetchEntitySourcesWithCache = ( export const fetchEntitySourcesWithCache = (
hass: HomeAssistant, hass: HomeAssistant
entity_id?: string
): Promise<EntitySources> => ): Promise<EntitySources> =>
entity_id timeCachePromiseFunc(
? fetchEntitySources(hass, entity_id) "_entitySources",
: timeCachePromiseFunc( // cache for 30 seconds
"_entitySources", 30000,
// cache for 30 seconds fetchEntitySources,
30000, // We base the cache on number of states. If number of states
fetchEntitySources, // changes we force a refresh
// We base the cache on number of states. If number of states (hass2) => Object.keys(hass2.states).length,
// changes we force a refresh hass
(hass2) => Object.keys(hass2.states).length, );
hass
);

View File

@@ -5,14 +5,12 @@ import {
DOMAINS_WITH_DYNAMIC_PICTURE, DOMAINS_WITH_DYNAMIC_PICTURE,
} from "../common/const"; } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { autoCaseNoun } from "../common/translations/auto_case_noun"; import { autoCaseNoun } from "../common/translations/auto_case_noun";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker"; import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { UNAVAILABLE, UNKNOWN } from "./entity"; import { UNAVAILABLE, UNKNOWN } from "./entity";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages"; const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"]; export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"];
@@ -339,14 +337,9 @@ export const localizeStateMessage = (
// TODO: This is not working yet, as we don't get historic attribute values // TODO: This is not working yet, as we don't get historic attribute values
const event_type = computeAttributeValueDisplay( const event_type = hass
hass!.localize, .formatEntityAttributeValue(stateObj, "event_type")
stateObj, ?.toString();
hass.locale,
hass.config,
hass.entities,
"event_type"
)?.toString();
if (!event_type) { if (!event_type) {
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`); return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`);
@@ -392,16 +385,7 @@ export const localizeStateMessage = (
return hass.localize( return hass.localize(
`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`, `${LOGBOOK_LOCALIZE_PATH}.changed_to_state`,
"state", "state",
stateObj stateObj ? hass.formatEntityState(stateObj, state) : state
? computeStateDisplay(
localize,
stateObj,
hass.locale,
hass.config,
hass.entities,
state
)
: state
); );
}; };

View File

@@ -5,7 +5,6 @@ import {
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import durationToSeconds from "../common/datetime/duration_to_seconds"; import durationToSeconds from "../common/datetime/duration_to_seconds";
import secondsToDuration from "../common/datetime/seconds_to_duration"; import secondsToDuration from "../common/datetime/seconds_to_duration";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
export type TimerEntity = HassEntityBase & { export type TimerEntity = HassEntityBase & {
@@ -90,25 +89,13 @@ export const computeDisplayTimer = (
} }
if (stateObj.state === "idle" || timeRemaining === 0) { if (stateObj.state === "idle" || timeRemaining === 0) {
return computeStateDisplay( return hass.formatEntityState(stateObj);
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities
);
} }
let display = secondsToDuration(timeRemaining || 0); let display = secondsToDuration(timeRemaining || 0);
if (stateObj.state === "paused") { if (stateObj.state === "paused") {
display = `${display} (${computeStateDisplay( display = `${display} (${hass.formatEntityState(stateObj)})`;
hass.localize,
stateObj,
hass.locale,
hass.config,
hass.entities
)})`;
} }
return display; return display;

View File

@@ -36,7 +36,9 @@ export const enum WeatherEntityFeature {
FORECAST_TWICE_DAILY = 4, FORECAST_TWICE_DAILY = 4,
} }
export type ForecastType = "legacy" | "hourly" | "daily" | "twice_daily"; export type ModernForecastType = "hourly" | "daily" | "twice_daily";
export type ForecastType = ModernForecastType | "legacy";
interface ForecastAttribute { interface ForecastAttribute {
temperature: number; temperature: number;
@@ -636,7 +638,7 @@ export const getForecast = (
export const subscribeForecast = ( export const subscribeForecast = (
hass: HomeAssistant, hass: HomeAssistant,
entity_id: string, entity_id: string,
forecast_type: "daily" | "hourly" | "twice_daily", forecast_type: ModernForecastType,
callback: (forecastevent: ForecastEvent) => void callback: (forecastevent: ForecastEvent) => void
) => ) =>
hass.connection.subscribeMessage<ForecastEvent>(callback, { hass.connection.subscribeMessage<ForecastEvent>(callback, {
@@ -645,15 +647,31 @@ export const subscribeForecast = (
entity_id, entity_id,
}); });
export const getSupportedForecastTypes = (
stateObj: HassEntityBase
): ModernForecastType[] => {
const supported: ModernForecastType[] = [];
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
supported.push("daily");
}
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
supported.push("twice_daily");
}
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
supported.push("hourly");
}
return supported;
};
export const getDefaultForecastType = (stateObj: HassEntityBase) => { export const getDefaultForecastType = (stateObj: HassEntityBase) => {
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) { if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
return "daily"; return "daily";
} }
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
return "hourly";
}
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) { if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
return "twice_daily"; return "twice_daily";
} }
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
return "hourly";
}
return undefined; return undefined;
}; };

View File

@@ -1,29 +1,65 @@
import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket"; import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
export interface RenderTemplateResult { export interface RenderTemplateResult {
result: string; result: string;
listeners: TemplateListeners; listeners: TemplateListeners;
} }
interface TemplateListeners { export interface RenderTemplateError {
error: string;
level: "ERROR" | "WARNING";
}
export interface TemplateListeners {
all: boolean; all: boolean;
domains: string[]; domains: string[];
entities: string[]; entities: string[];
time: boolean; time: boolean;
} }
export type TemplatePreview = TemplatePreviewState | TemplatePreviewError;
interface TemplatePreviewState {
state: string;
attributes: Record<string, any>;
listeners: TemplateListeners;
}
interface TemplatePreviewError {
error: string;
}
export const subscribeRenderTemplate = ( export const subscribeRenderTemplate = (
conn: Connection, conn: Connection,
onChange: (result: RenderTemplateResult) => void, onChange: (result: RenderTemplateResult | RenderTemplateError) => void,
params: { params: {
template: string; template: string;
entity_ids?: string | string[]; entity_ids?: string | string[];
variables?: Record<string, unknown>; variables?: Record<string, unknown>;
timeout?: number; timeout?: number;
strict?: boolean; strict?: boolean;
report_errors?: boolean;
} }
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), { conn.subscribeMessage(
type: "render_template", (msg: RenderTemplateResult | RenderTemplateError) => onChange(msg),
...params, {
type: "render_template",
...params,
}
);
export const subscribePreviewTemplate = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: TemplatePreview) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "template/start_preview",
flow_id,
flow_type,
user_input,
}); });

View File

@@ -404,8 +404,6 @@ export interface RequestedGrant {
clientSideAuth: boolean; clientSideAuth: boolean;
} }
export const nodeStatus = ["unknown", "asleep", "awake", "dead", "alive"];
export const fetchZwaveNetworkStatus = ( export const fetchZwaveNetworkStatus = (
hass: HomeAssistant, hass: HomeAssistant,
device_or_entry_id: { device_or_entry_id: {

View File

@@ -1,7 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { isUnavailableState } from "../../../data/entity"; import { isUnavailableState } from "../../../data/entity";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor"; import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
@@ -21,6 +20,7 @@ class EntityPreviewRow extends LitElement {
return html`<state-badge return html`<state-badge
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .stateObj=${stateObj}
stateColor
></state-badge> ></state-badge>
<div class="name" .title=${computeStateName(stateObj)}> <div class="name" .title=${computeStateName(stateObj)}>
${computeStateName(stateObj)} ${computeStateName(stateObj)}
@@ -35,13 +35,7 @@ class EntityPreviewRow extends LitElement {
capitalize capitalize
></hui-timestamp-display> ></hui-timestamp-display>
` `
: computeStateDisplay( : this.hass.formatEntityState(stateObj)}
this.hass!.localize,
stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
)}
</div>`; </div>`;
} }

View File

@@ -49,7 +49,7 @@ class FlowPreviewGroup extends LitElement {
private _setPreview = (preview: GroupPreview) => { private _setPreview = (preview: GroupPreview) => {
const now = new Date().toISOString(); const now = new Date().toISOString();
this._preview = { this._preview = {
entity_id: `${this.stepId}.flow_preview`, entity_id: `${this.stepId}.___flow_preview___`,
last_changed: now, last_changed: now,
last_updated: now, last_updated: now,
context: { id: "", parent_id: null, user_id: null }, context: { id: "", parent_id: null, user_id: null },

View File

@@ -0,0 +1,179 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { debounce } from "../../../common/util/debounce";
import { FlowType } from "../../../data/data_entry_flow";
import {
TemplateListeners,
TemplatePreview,
subscribePreviewTemplate,
} from "../../../data/ws-templates";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
import { fireEvent } from "../../../common/dom/fire_event";
@customElement("flow-preview-template")
class FlowPreviewTemplate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
@property() public stepId!: string;
@property() public flowId!: string;
@property() public stepData!: Record<string, any>;
@state() private _preview?: HassEntity;
@state() private _listeners?: TemplateListeners;
@state() private _error?: string;
private _unsub?: Promise<UnsubscribeFunc>;
disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._debouncedSubscribePreview();
}
}
protected render() {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html`<entity-preview-row
.hass=${this.hass}
.stateObj=${this._preview}
></entity-preview-row>
${this._listeners?.time
? html`
<p>
${this.hass.localize("ui.dialogs.helper_settings.template.time")}
</p>
`
: nothing}
${!this._listeners
? nothing
: this._listeners.all
? html`
<p class="all_listeners">
${this.hass.localize(
"ui.dialogs.helper_settings.template.all_listeners"
)}
</p>
`
: this._listeners.domains.length || this._listeners.entities.length
? html`
<p>
${this.hass.localize(
"ui.dialogs.helper_settings.template.listeners"
)}
</p>
<ul>
${this._listeners.domains
.sort()
.map(
(domain) => html`
<li>
<b
>${this.hass.localize(
"ui.dialogs.helper_settings.template.domain"
)}</b
>: ${domain}
</li>
`
)}
${this._listeners.entities
.sort()
.map(
(entity_id) => html`
<li>
<b
>${this.hass.localize(
"ui.dialogs.helper_settings.template.entity"
)}</b
>: ${entity_id}
</li>
`
)}
</ul>
`
: !this._listeners.time
? html`<p class="all_listeners">
${this.hass.localize(
"ui.dialogs.helper_settings.template.no_listeners"
)}
</p>`
: nothing} `;
}
private _setPreview = (preview: TemplatePreview) => {
if ("error" in preview) {
this._error = preview.error;
this._preview = undefined;
return;
}
this._error = undefined;
this._listeners = preview.listeners;
const now = new Date().toISOString();
this._preview = {
entity_id: `${this.stepId}.___flow_preview___`,
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
attributes: preview.attributes,
state: preview.state,
};
};
private _debouncedSubscribePreview = debounce(() => {
this._subscribePreview();
}, 250);
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
return;
}
try {
this._unsub = subscribePreviewTemplate(
this.hass,
this.flowId,
this.flowType,
this.stepData,
this._setPreview
);
await this._unsub;
fireEvent(this, "set-flow-errors", { errors: {} });
} catch (err: any) {
if (typeof err.message === "string") {
this._error = err.message;
} else {
this._error = undefined;
fireEvent(this, "set-flow-errors", err.message);
}
this._unsub = undefined;
this._preview = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"flow-preview-template": FlowPreviewTemplate;
}
}

View File

@@ -1,6 +1,6 @@
import { html } from "lit"; import { html } from "lit";
import { ConfigEntry } from "../../data/config_entries"; import { ConfigEntry } from "../../data/config_entries";
import { domainToName, IntegrationManifest } from "../../data/integration"; import { domainToName } from "../../data/integration";
import { import {
createOptionsFlow, createOptionsFlow,
deleteOptionsFlow, deleteOptionsFlow,
@@ -8,6 +8,7 @@ import {
handleOptionsFlowStep, handleOptionsFlowStep,
} from "../../data/options_flow"; } from "../../data/options_flow";
import { import {
DataEntryFlowDialogParams,
loadDataEntryFlowDialog, loadDataEntryFlowDialog,
showFlowDialog, showFlowDialog,
} from "./show-dialog-data-entry-flow"; } from "./show-dialog-data-entry-flow";
@@ -17,14 +18,14 @@ export const loadOptionsFlowDialog = loadDataEntryFlowDialog;
export const showOptionsFlowDialog = ( export const showOptionsFlowDialog = (
element: HTMLElement, element: HTMLElement,
configEntry: ConfigEntry, configEntry: ConfigEntry,
manifest?: IntegrationManifest | null dialogParams?: Omit<DataEntryFlowDialogParams, "flowConfig">
): void => ): void =>
showFlowDialog( showFlowDialog(
element, element,
{ {
startFlowHandler: configEntry.entry_id, startFlowHandler: configEntry.entry_id,
domain: configEntry.domain, domain: configEntry.domain,
manifest, ...dialogParams,
}, },
{ {
flowType: "options_flow", flowType: "options_flow",

View File

@@ -70,7 +70,7 @@ class StepFlowForm extends LitElement {
></ha-form> ></ha-form>
</div> </div>
${step.preview ${step.preview
? html`<div class="preview"> ? html`<div class="preview" @set-flow-errors=${this._setError}>
<h3> <h3>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.config_flow.preview" "ui.panel.config.integrations.config_flow.preview"
@@ -107,6 +107,10 @@ class StepFlowForm extends LitElement {
`; `;
} }
private _setError(ev: CustomEvent) {
this.step = { ...this.step, errors: ev.detail };
}
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0); setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0);
@@ -253,6 +257,9 @@ class StepFlowForm extends LitElement {
} }
declare global { declare global {
interface HASSDomEvents {
"set-flow-errors": { errors: DataEntryFlowStepForm["errors"] };
}
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"step-flow-form": StepFlowForm; "step-flow-form": StepFlowForm;
} }

View File

@@ -11,7 +11,6 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../../../common/const"; import { UNIT_F } from "../../../../common/const";
import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display";
import { stateActive } from "../../../../common/entity/state_active"; import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color"; import { stateColorCss } from "../../../../common/entity/state_color";
import { supportsFeature } from "../../../../common/entity/supports-feature"; import { supportsFeature } from "../../../../common/entity/supports-feature";
@@ -162,14 +161,10 @@ export class HaMoreInfoClimateTemperature extends LitElement {
const action = this.stateObj.attributes.hvac_action; const action = this.stateObj.attributes.hvac_action;
const actionLabel = computeAttributeValueDisplay( const actionLabel = this.hass.formatEntityAttributeValue(
this.hass.localize,
this.stateObj, this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"hvac_action" "hvac_action"
) as string; );
return html` return html`
<p class="label"> <p class="label">
@@ -280,15 +275,21 @@ export class HaMoreInfoClimateTemperature extends LitElement {
); );
} }
const activeModes = this.stateObj.attributes.hvac_modes.filter(
(m) => m !== "off"
);
if ( if (
supportsTargetTemperature && supportsTargetTemperature &&
this._targetTemperature.value != null && this._targetTemperature.value != null &&
this.stateObj.state !== UNAVAILABLE this.stateObj.state !== UNAVAILABLE
) { ) {
const heatCoolModes = this.stateObj.attributes.hvac_modes.filter((m) =>
["heat", "cool", "heat_cool"].includes(m)
);
const sliderMode =
SLIDER_MODES[
heatCoolModes.length === 1 && ["off", "auto"].includes(mode)
? heatCoolModes[0]
: mode
];
return html` return html`
<div <div
class="container" class="container"
@@ -299,9 +300,7 @@ export class HaMoreInfoClimateTemperature extends LitElement {
> >
<ha-control-circular-slider <ha-control-circular-slider
.inactive=${!active} .inactive=${!active}
.mode=${mode === "off" && activeModes.length === 1 .mode=${sliderMode}
? SLIDER_MODES[activeModes[0]]
: SLIDER_MODES[mode]}
.value=${this._targetTemperature.value} .value=${this._targetTemperature.value}
.min=${this._min} .min=${this._min}
.max=${this._max} .max=${this._max}

View File

@@ -35,9 +35,8 @@ export class HaMoreInfoCoverPosition extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const forcedState = this.stateObj.state === "closed" ? "open" : undefined; const openColor = stateColorCss(this.stateObj, "open");
const color = stateColorCss(this.stateObj);
const color = stateColorCss(this.stateObj, forcedState);
return html` return html`
<ha-control-slider <ha-control-slider
@@ -55,6 +54,8 @@ export class HaMoreInfoCoverPosition extends LitElement {
"current_position" "current_position"
)} )}
style=${styleMap({ style=${styleMap({
// Use open color for inactive state to avoid grey slider that looks disabled
"--state-cover-inactive-color": openColor,
"--control-slider-color": color, "--control-slider-color": color,
"--control-slider-background": color, "--control-slider-background": color,
})} })}

View File

@@ -72,9 +72,8 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const forcedState = this.stateObj.state === "closed" ? "open" : undefined; const openColor = stateColorCss(this.stateObj, "open");
const color = stateColorCss(this.stateObj);
const color = stateColorCss(this.stateObj, forcedState);
return html` return html`
<ha-control-slider <ha-control-slider
@@ -91,6 +90,8 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
"current_tilt_position" "current_tilt_position"
)} )}
style=${styleMap({ style=${styleMap({
// Use open color for inactive state to avoid grey slider that looks disabled
"--state-cover-inactive-color": openColor,
"--control-slider-color": color, "--control-slider-color": color,
"--control-slider-background": color, "--control-slider-background": color,
})} })}

View File

@@ -2,7 +2,6 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display"; import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../../common/entity/compute_state_display";
import { stateActive } from "../../../../common/entity/state_active"; import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color"; import { stateColorCss } from "../../../../common/entity/state_color";
import "../../../../components/ha-control-select"; import "../../../../components/ha-control-select";
@@ -12,12 +11,12 @@ import { UNAVAILABLE } from "../../../../data/entity";
import { import {
computeFanSpeedCount, computeFanSpeedCount,
computeFanSpeedIcon, computeFanSpeedIcon,
FAN_SPEED_COUNT_MAX_FOR_BUTTONS,
FAN_SPEEDS,
FanEntity, FanEntity,
fanPercentageToSpeed, fanPercentageToSpeed,
FanSpeed, FanSpeed,
fanSpeedToPercentage, fanSpeedToPercentage,
FAN_SPEEDS,
FAN_SPEED_COUNT_MAX_FOR_BUTTONS,
} from "../../../../data/fan"; } from "../../../../data/fan";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@@ -68,14 +67,7 @@ export class HaMoreInfoFanSpeed extends LitElement {
private _localizeSpeed(speed: FanSpeed) { private _localizeSpeed(speed: FanSpeed) {
if (speed === "on" || speed === "off") { if (speed === "on" || speed === "off") {
return computeStateDisplay( return this.hass.formatEntityState(this.stateObj, speed);
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
speed
);
} }
return ( return (
this.hass.localize(`ui.dialogs.more_info_control.fan.speed.${speed}`) || this.hass.localize(`ui.dialogs.more_info_control.fan.speed.${speed}`) ||

View File

@@ -34,6 +34,10 @@ export const moreInfoControlStyle = css`
} }
ha-attributes { ha-attributes {
display: block;
width: 100%; width: 100%;
} }
ha-more-info-control-select-container + ha-attributes:not([empty]) {
margin-top: 16px;
}
`; `;

View File

@@ -1,7 +1,5 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import "../../../components/ha-absolute-time"; import "../../../components/ha-absolute-time";
import "../../../components/ha-relative-time"; import "../../../components/ha-relative-time";
import { isUnavailableState } from "../../../data/entity"; import { isUnavailableState } from "../../../data/entity";
@@ -20,30 +18,22 @@ export class HaMoreInfoStateHeader extends LitElement {
@state() private _absoluteTime = false; @state() private _absoluteTime = false;
private _computeStateDisplay(stateObj: HassEntity): TemplateResult | string { private _localizeState(): TemplateResult | string {
if ( if (
stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP && this.stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
!isUnavailableState(stateObj.state) !isUnavailableState(this.stateObj.state)
) { ) {
return html` return html`
<hui-timestamp-display <hui-timestamp-display
.hass=${this.hass} .hass=${this.hass}
.ts=${new Date(stateObj.state)} .ts=${new Date(this.stateObj.state)}
format="relative" format="relative"
capitalize capitalize
></hui-timestamp-display> ></hui-timestamp-display>
`; `;
} }
const stateDisplay = computeStateDisplay( return this.hass.formatEntityState(this.stateObj);
this.hass!.localize,
stateObj,
this.hass!.locale,
this.hass!.config,
this.hass!.entities
);
return stateDisplay;
} }
private _toggleAbsolute() { private _toggleAbsolute() {
@@ -51,8 +41,7 @@ export class HaMoreInfoStateHeader extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const stateDisplay = const stateDisplay = this.stateOverride ?? this._localizeState();
this.stateOverride ?? this._computeStateDisplay(this.stateObj);
return html` return html`
<p class="state">${stateDisplay}</p> <p class="state">${stateDisplay}</p>

View File

@@ -2,7 +2,6 @@ import { mdiMinus, mdiPlus } from "@mdi/js";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display";
import { stateActive } from "../../../../common/entity/state_active"; import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color"; import { stateColorCss } from "../../../../common/entity/state_color";
import { clamp } from "../../../../common/number/clamp"; import { clamp } from "../../../../common/number/clamp";
@@ -92,14 +91,10 @@ export class HaMoreInfoHumidifierHumidity extends LitElement {
const action = this.stateObj.attributes.action; const action = this.stateObj.attributes.action;
const actionLabel = computeAttributeValueDisplay( const actionLabel = this.hass.formatEntityAttributeValue(
this.hass.localize,
this.stateObj, this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"action" "action"
) as string; );
return html` return html`
<p class="label"> <p class="label">

View File

@@ -1,6 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import { mdiEyedropper } from "@mdi/js"; import { mdiEyedropper } from "@mdi/js";
import { import {
css, css,
@@ -26,7 +24,6 @@ import "../../../../components/ha-hs-color-picker";
import "../../../../components/ha-icon"; import "../../../../components/ha-icon";
import "../../../../components/ha-icon-button-prev"; import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-labeled-slider"; import "../../../../components/ha-labeled-slider";
import "../../../../components/ha-temp-color-picker";
import { import {
getLightCurrentModeRgbColor, getLightCurrentModeRgbColor,
LightColor, LightColor,

View File

@@ -1,25 +1,31 @@
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
nothing,
PropertyValues, PropertyValues,
css,
html,
nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { rgb2hex } from "../../../../common/color/convert-color";
import {
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
temperature2rgb,
} from "../../../../common/color/convert-light-color";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stateColorCss } from "../../../../common/entity/state_color";
import { throttle } from "../../../../common/util/throttle"; import { throttle } from "../../../../common/util/throttle";
import "../../../../components/ha-temp-color-picker"; import "../../../../components/ha-control-slider";
import { UNAVAILABLE } from "../../../../data/entity";
import { import {
LightColor, LightColor,
LightColorMode, LightColorMode,
LightEntity, LightEntity,
} from "../../../../data/light"; } from "../../../../data/light";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import {
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
} from "../../../../common/color/convert-light-color";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@@ -28,6 +34,26 @@ declare global {
} }
} }
export const generateColorTemperatureGradient = (min: number, max: number) => {
const count = 10;
const gradient: [number, string][] = [];
const step = (max - min) / count;
const percentageStep = 1 / count;
for (let i = 0; i < count + 1; i++) {
const value = min + step * i;
const hex = rgb2hex(temperature2rgb(value));
gradient.push([percentageStep * i, hex]);
}
return gradient
.map(([stop, color]) => `${color} ${(stop as number) * 100}%`)
.join(", ");
};
@customElement("light-color-temp-picker") @customElement("light-color-temp-picker")
class LightColorTempPicker extends LitElement { class LightColorTempPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -46,18 +72,36 @@ class LightColorTempPicker extends LitElement {
const maxKelvin = const maxKelvin =
this.stateObj.attributes.max_color_temp_kelvin ?? DEFAULT_MAX_KELVIN; this.stateObj.attributes.max_color_temp_kelvin ?? DEFAULT_MAX_KELVIN;
const gradient = this._generateTemperatureGradient(minKelvin!, maxKelvin);
const color = stateColorCss(this.stateObj);
return html` return html`
<ha-temp-color-picker <ha-control-slider
@value-changed=${this._ctColorChanged} inverted
@cursor-moved=${this._ctColorCursorMoved} vertical
.value=${this._ctPickerValue}
.min=${minKelvin} .min=${minKelvin}
.max=${maxKelvin} .max=${maxKelvin}
.value=${this._ctPickerValue} mode="cursor"
@value-changed=${this._ctColorChanged}
@slider-moved=${this._ctColorCursorMoved}
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.light.color_temp"
)}
style=${styleMap({
"--control-slider-color": color,
"--gradient": gradient,
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
> >
</ha-temp-color-picker> </ha-control-slider>
`; `;
} }
private _generateTemperatureGradient = memoizeOne(
(min: number, max: number) => generateColorTemperatureGradient(min, max)
);
public _updateSliderValues() { public _updateSliderValues() {
const stateObj = this.stateObj; const stateObj = this.stateObj;
@@ -138,10 +182,18 @@ class LightColorTempPicker extends LitElement {
flex-direction: column; flex-direction: column;
} }
ha-temp-color-picker { ha-control-slider {
height: 45vh; height: 45vh;
max-height: 320px; max-height: 320px;
min-height: 200px; min-height: 200px;
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--control-slider-color: var(--primary-color);
--control-slider-background: -webkit-linear-gradient(
top,
var(--gradient)
);
--control-slider-background-opacity: 1;
} }
`, `,
]; ];

View File

@@ -142,7 +142,7 @@ class MoreInfoClimate extends LitElement {
.selected=${this._mainControl === "temperature"} .selected=${this._mainControl === "temperature"}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.light.color" "ui.dialogs.more_info_control.climate.temperature"
)} )}
.control=${"temperature"} .control=${"temperature"}
@click=${this._setMainControl} @click=${this._setMainControl}
@@ -153,7 +153,7 @@ class MoreInfoClimate extends LitElement {
.selected=${this._mainControl === "humidity"} .selected=${this._mainControl === "humidity"}
.disabled=${this.stateObj!.state === UNAVAILABLE} .disabled=${this.stateObj!.state === UNAVAILABLE}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.light.color_temp" "ui.dialogs.more_info_control.climate.humidity"
)} )}
.control=${"humidity"} .control=${"humidity"}
@click=${this._setMainControl} @click=${this._setMainControl}
@@ -166,10 +166,7 @@ class MoreInfoClimate extends LitElement {
</div> </div>
<ha-more-info-control-select-container> <ha-more-info-control-select-container>
<ha-control-select-menu <ha-control-select-menu
.label=${this.hass.formatEntityAttributeName( .label=${this.hass.localize("ui.card.climate.mode")}
this.stateObj,
"hvac_mode"
)}
.value=${stateObj.state} .value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition fixedMenuPosition

View File

@@ -1,22 +1,21 @@
import { mdiMenu, mdiSwapVertical } from "@mdi/js"; import { mdiMenu, mdiSwapVertical } from "@mdi/js";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
nothing,
PropertyValues, PropertyValues,
css,
html,
nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attributes"; import "../../../components/ha-attributes";
import "../../../components/ha-icon-button-group"; import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle"; import "../../../components/ha-icon-button-toggle";
import { import {
computeCoverPositionStateDisplay,
CoverEntity, CoverEntity,
CoverEntityFeature, CoverEntityFeature,
computeCoverPositionStateDisplay,
} from "../../../data/cover"; } from "../../../data/cover";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../components/cover/ha-more-info-cover-buttons"; import "../components/cover/ha-more-info-cover-buttons";
@@ -83,12 +82,8 @@ class MoreInfoCover extends LitElement {
const forcedState = const forcedState =
liveValue != null ? (liveValue ? "open" : "closed") : undefined; liveValue != null ? (liveValue ? "open" : "closed") : undefined;
const stateDisplay = computeStateDisplay( const stateDisplay = this.hass.formatEntityState(
this.hass.localize,
this.stateObj!, this.stateObj!,
this.hass.locale,
this.hass.config,
this.hass.entities,
forcedState forcedState
); );

View File

@@ -86,7 +86,7 @@ class MoreInfoFan extends LitElement {
} }
_handleOscillating(ev) { _handleOscillating(ev) {
const newVal = ev.target.value === "on"; const newVal = ev.target.value === "true";
this.hass.callService("fan", "oscillate", { this.hass.callService("fan", "oscillate", {
entity_id: this.stateObj!.entity_id, entity_id: this.stateObj!.entity_id,
@@ -269,7 +269,9 @@ class MoreInfoFan extends LitElement {
this.stateObj, this.stateObj,
"oscillating" "oscillating"
)} )}
.value=${this.stateObj.attributes.oscillating ? "on" : "off"} .value=${this.stateObj.attributes.oscillating
? "true"
: "false"}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
@@ -280,19 +282,27 @@ class MoreInfoFan extends LitElement {
slot="icon" slot="icon"
.path=${haOscillatingOff} .path=${haOscillatingOff}
></ha-svg-icon> ></ha-svg-icon>
<ha-list-item value="on" graphic="icon"> <ha-list-item value="true" graphic="icon">
<ha-svg-icon <ha-svg-icon
slot="graphic" slot="graphic"
.path=${haOscillating} .path=${haOscillating}
></ha-svg-icon> ></ha-svg-icon>
${this.hass.localize("state.default.on")} ${this.hass.formatEntityAttributeValue(
this.stateObj,
"oscillating",
true
)}
</ha-list-item> </ha-list-item>
<ha-list-item value="off" graphic="icon"> <ha-list-item value="false" graphic="icon">
<ha-svg-icon <ha-svg-icon
slot="graphic" slot="graphic"
.path=${haOscillatingOff} .path=${haOscillatingOff}
></ha-svg-icon> ></ha-svg-icon>
${this.hass.localize("state.default.off")} ${this.hass.formatEntityAttributeValue(
this.stateObj,
"oscillating",
false
)}
</ha-list-item> </ha-list-item>
</ha-control-select-menu> </ha-control-select-menu>
` `

View File

@@ -10,11 +10,6 @@ import {
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import {
computeAttributeNameDisplay,
computeAttributeValueDisplay,
} from "../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-select-menu"; import "../../../components/ha-control-select-menu";
import "../../../components/ha-list-item"; import "../../../components/ha-list-item";
@@ -58,26 +53,21 @@ class MoreInfoHumidifier extends LitElement {
HumidifierEntityFeature.MODES HumidifierEntityFeature.MODES
); );
const currentHumidity = this.stateObj.attributes.current_humidity as number;
return html` return html`
<div class="current"> <div class="current">
${currentHumidity != null ${this.stateObj.attributes.current_humidity != null
? html` ? html`
<div> <div>
<p class="label"> <p class="label">
${computeAttributeNameDisplay( ${this.hass.formatEntityAttributeName(
this.hass.localize,
this.stateObj, this.stateObj,
this.hass.entities,
"current_humidity" "current_humidity"
)} )}
</p> </p>
<p class="value"> <p class="value">
${this.hass.formatEntityAttributeValue( ${this.hass.formatEntityAttributeValue(
this.stateObj, this.stateObj,
"current_humidity", "current_humidity"
currentHumidity
)} )}
</p> </p>
</div> </div>
@@ -104,24 +94,10 @@ class MoreInfoHumidifier extends LitElement {
> >
<ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon>
<ha-list-item value="off"> <ha-list-item value="off">
${computeStateDisplay( ${this.hass.formatEntityState(this.stateObj, "off")}
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"off"
)}
</ha-list-item> </ha-list-item>
<ha-list-item value="on"> <ha-list-item value="on">
${computeStateDisplay( ${this.hass.formatEntityState(this.stateObj, "on")}
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"on"
)}
</ha-list-item> </ha-list-item>
</ha-control-select-menu> </ha-control-select-menu>
@@ -144,12 +120,8 @@ class MoreInfoHumidifier extends LitElement {
slot="graphic" slot="graphic"
.path=${computeHumidiferModeIcon(mode)} .path=${computeHumidiferModeIcon(mode)}
></ha-svg-icon> ></ha-svg-icon>
${computeAttributeValueDisplay( ${this.hass.formatEntityAttributeValue(
hass.localize,
stateObj!, stateObj!,
hass.locale,
hass.config,
hass.entities,
"mode", "mode",
mode mode
)} )}

View File

@@ -2,7 +2,6 @@ import { mdiHomeImportOutline, mdiPause, mdiPlay } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
@@ -74,15 +73,7 @@ class MoreInfoLawnMower extends LitElement {
)}: )}:
</span> </span>
<span> <span>
<strong> <strong>${this.hass.formatEntityState(stateObj)}</strong>
${computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
)}
</strong>
</span> </span>
</div> </div>
${this._renderBattery()} ${this._renderBattery()}

View File

@@ -9,24 +9,23 @@ import {
mdiVolumeOff, mdiVolumeOff,
mdiVolumePlus, mdiVolumePlus,
} from "@mdi/js"; } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display"; import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { stateActive } from "../../../common/entity/state_active";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-select"; import "../../../components/ha-select";
import "../../../components/ha-slider"; import "../../../components/ha-slider";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog"; import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
import { import {
computeMediaControls,
handleMediaControlClick,
MediaPickedEvent, MediaPickedEvent,
MediaPlayerEntity, MediaPlayerEntity,
MediaPlayerEntityFeature, MediaPlayerEntityFeature,
computeMediaControls,
handleMediaControlClick,
mediaPlayerPlayMedia, mediaPlayerPlayMedia,
} from "../../../data/media-player"; } from "../../../data/media-player";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
@@ -157,24 +156,20 @@ class MoreInfoMediaPlayer extends LitElement {
> >
${stateObj.attributes.source_list!.map( ${stateObj.attributes.source_list!.map(
(source) => html` (source) => html`
<mwc-list-item .value=${source} <mwc-list-item .value=${source}>
>${computeAttributeValueDisplay( ${this.hass.formatEntityAttributeValue(
this.hass.localize,
stateObj, stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"source", "source",
source source
)}</mwc-list-item )}
> </mwc-list-item>
` `
)} )}
<ha-svg-icon .path=${mdiLoginVariant} slot="icon"></ha-svg-icon> <ha-svg-icon .path=${mdiLoginVariant} slot="icon"></ha-svg-icon>
</ha-select> </ha-select>
</div> </div>
` `
: ""} : nothing}
${stateActive(stateObj) && ${stateActive(stateObj) &&
supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOUND_MODE) && supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOUND_MODE) &&
stateObj.attributes.sound_mode_list?.length stateObj.attributes.sound_mode_list?.length
@@ -191,17 +186,13 @@ class MoreInfoMediaPlayer extends LitElement {
> >
${stateObj.attributes.sound_mode_list.map( ${stateObj.attributes.sound_mode_list.map(
(mode) => html` (mode) => html`
<mwc-list-item .value=${mode} <mwc-list-item .value=${mode}>
>${computeAttributeValueDisplay( ${this.hass.formatEntityAttributeValue(
this.hass.localize,
stateObj, stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"sound_mode", "sound_mode",
mode mode
)}</mwc-list-item )}
> </mwc-list-item>
` `
)} )}
<ha-svg-icon .path=${mdiMusicNote} slot="icon"></ha-svg-icon> <ha-svg-icon .path=${mdiMusicNote} slot="icon"></ha-svg-icon>

View File

@@ -2,11 +2,10 @@ import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attributes"; import "../../../components/ha-attributes";
import { RemoteEntity, REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote"; import { REMOTE_SUPPORT_ACTIVITY, RemoteEntity } from "../../../data/remote";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
const filterExtraAttributes = "activity_list,current_activity"; const filterExtraAttributes = "activity_list,current_activity";
@@ -40,12 +39,8 @@ class MoreInfoRemote extends LitElement {
${stateObj.attributes.activity_list!.map( ${stateObj.attributes.activity_list!.map(
(activity) => html` (activity) => html`
<mwc-list-item .value=${activity}> <mwc-list-item .value=${activity}>
${computeAttributeValueDisplay( ${this.hass.formatEntityAttributeValue(
this.hass.localize,
stateObj, stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"activity", "activity",
activity activity
)} )}
@@ -54,7 +49,7 @@ class MoreInfoRemote extends LitElement {
)} )}
</mwc-list> </mwc-list>
` `
: ""} : nothing}
<ha-attributes <ha-attributes
.hass=${this.hass} .hass=${this.hass}

View File

@@ -13,8 +13,6 @@ import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/entity/ha-battery-icon"; import "../../../components/entity/ha-battery-icon";
@@ -127,21 +125,8 @@ class MoreInfoVacuum extends LitElement {
<strong> <strong>
${supportsFeature(stateObj, VacuumEntityFeature.STATUS) && ${supportsFeature(stateObj, VacuumEntityFeature.STATUS) &&
stateObj.attributes.status stateObj.attributes.status
? computeAttributeValueDisplay( ? this.hass.formatEntityAttributeValue(stateObj, "status")
this.hass.localize, : this.hass.formatEntityState(stateObj)}
stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"status"
)
: computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
)}
</strong> </strong>
</span> </span>
</div> </div>
@@ -197,12 +182,8 @@ class MoreInfoVacuum extends LitElement {
${stateObj.attributes.fan_speed_list!.map( ${stateObj.attributes.fan_speed_list!.map(
(mode) => html` (mode) => html`
<mwc-list-item .value=${mode}> <mwc-list-item .value=${mode}>
${computeAttributeValueDisplay( ${this.hass.formatEntityAttributeValue(
this.hass.localize,
stateObj, stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"fan_speed", "fan_speed",
mode mode
)} )}
@@ -215,12 +196,8 @@ class MoreInfoVacuum extends LitElement {
> >
<span> <span>
<ha-svg-icon .path=${mdiFan}></ha-svg-icon> <ha-svg-icon .path=${mdiFan}></ha-svg-icon>
${computeAttributeValueDisplay( ${this.hass.formatEntityAttributeValue(
this.hass.localize,
stateObj, stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"fan_speed" "fan_speed"
)} )}
</span> </span>

View File

@@ -73,10 +73,7 @@ class MoreInfoWaterHeater extends LitElement {
${supportOperationMode && stateObj.attributes.operation_list ${supportOperationMode && stateObj.attributes.operation_list
? html` ? html`
<ha-control-select-menu <ha-control-select-menu
.label=${this.hass.formatEntityAttributeName( .label=${this.hass.localize("ui.card.water_heater.mode")}
stateObj,
"operation"
)}
.value=${stateObj.state} .value=${stateObj.state}
.disabled=${stateObj.state === UNAVAILABLE} .disabled=${stateObj.state === UNAVAILABLE}
fixedMenuPosition fixedMenuPosition
@@ -122,11 +119,19 @@ class MoreInfoWaterHeater extends LitElement {
slot="graphic" slot="graphic"
.path=${mdiAccountArrowRight} .path=${mdiAccountArrowRight}
></ha-svg-icon> ></ha-svg-icon>
${this.hass.localize("state.default.on")} ${this.hass.formatEntityAttributeValue(
stateObj,
"away_mode",
"on"
)}
</ha-list-item> </ha-list-item>
<ha-list-item value="off" graphic="icon"> <ha-list-item value="off" graphic="icon">
<ha-svg-icon slot="graphic" .path=${mdiAccount}></ha-svg-icon> <ha-svg-icon slot="graphic" .path=${mdiAccount}></ha-svg-icon>
${this.hass.localize("state.default.off")} ${this.hass.formatEntityAttributeValue(
stateObj,
"away_mode",
"off"
)}
</ha-list-item> </ha-list-item>
</ha-control-select-menu> </ha-control-select-menu>
` `

View File

@@ -1,3 +1,5 @@
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { import {
mdiEye, mdiEye,
mdiGauge, mdiGauge,
@@ -14,14 +16,17 @@ import {
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { formatDateWeekdayDay } from "../../../common/datetime/format_date"; import { formatDateWeekdayDay } from "../../../common/datetime/format_date";
import { formatTimeWeekday } from "../../../common/datetime/format_time"; import { formatTimeWeekday } from "../../../common/datetime/format_time";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { import {
ForecastEvent, ForecastEvent,
ModernForecastType,
WeatherEntity, WeatherEntity,
getDefaultForecastType, getDefaultForecastType,
getForecast, getForecast,
getSupportedForecastTypes,
getWind, getWind,
subscribeForecast, subscribeForecast,
weatherIcons, weatherIcons,
@@ -36,6 +41,8 @@ class MoreInfoWeather extends LitElement {
@state() private _forecastEvent?: ForecastEvent; @state() private _forecastEvent?: ForecastEvent;
@state() private _forecastType?: ModernForecastType;
@state() private _subscribed?: Promise<() => void>; @state() private _subscribed?: Promise<() => void>;
private _unsubscribeForecastEvents() { private _unsubscribeForecastEvents() {
@@ -43,25 +50,28 @@ class MoreInfoWeather extends LitElement {
this._subscribed.then((unsub) => unsub()); this._subscribed.then((unsub) => unsub());
this._subscribed = undefined; this._subscribed = undefined;
} }
this._forecastEvent = undefined;
} }
private async _subscribeForecastEvents() { private async _subscribeForecastEvents() {
this._unsubscribeForecastEvents(); this._unsubscribeForecastEvents();
if (!this.isConnected || !this.hass || !this.stateObj) { if (
!this.isConnected ||
!this.hass ||
!this.stateObj ||
!this._forecastType
) {
return; return;
} }
const forecastType = getDefaultForecastType(this.stateObj); this._subscribed = subscribeForecast(
if (forecastType) { this.hass!,
this._subscribed = subscribeForecast( this.stateObj!.entity_id,
this.hass!, this._forecastType,
this.stateObj!.entity_id, (event) => {
forecastType, this._forecastEvent = event;
(event) => { }
this._forecastEvent = event; );
}
);
}
} }
public connectedCallback() { public connectedCallback() {
@@ -93,10 +103,10 @@ class MoreInfoWeather extends LitElement {
return false; return false;
} }
protected updated(changedProps: PropertyValues): void { protected willUpdate(changedProps: PropertyValues): void {
super.updated(changedProps); super.willUpdate(changedProps);
if (changedProps.has("stateObj") || !this._subscribed) { if ((changedProps.has("stateObj") || !this._subscribed) && this.stateObj) {
const oldState = changedProps.get("stateObj") as const oldState = changedProps.get("stateObj") as
| WeatherEntity | WeatherEntity
| undefined; | undefined;
@@ -104,16 +114,25 @@ class MoreInfoWeather extends LitElement {
oldState?.entity_id !== this.stateObj?.entity_id || oldState?.entity_id !== this.stateObj?.entity_id ||
!this._subscribed !this._subscribed
) { ) {
this._forecastType = getDefaultForecastType(this.stateObj);
this._subscribeForecastEvents(); this._subscribeForecastEvents();
} }
} else if (changedProps.has("_forecastType")) {
this._subscribeForecastEvents();
} }
} }
private _supportedForecasts = memoizeOne((stateObj: WeatherEntity) =>
getSupportedForecastTypes(stateObj)
);
protected render() { protected render() {
if (!this.hass || !this.stateObj) { if (!this.hass || !this.stateObj) {
return nothing; return nothing;
} }
const supportedForecasts = this._supportedForecasts(this.stateObj);
const forecastData = getForecast( const forecastData = getForecast(
this.stateObj.attributes, this.stateObj.attributes,
this._forecastEvent this._forecastEvent
@@ -210,6 +229,23 @@ class MoreInfoWeather extends LitElement {
<div class="section"> <div class="section">
${this.hass.localize("ui.card.weather.forecast")}: ${this.hass.localize("ui.card.weather.forecast")}:
</div> </div>
${supportedForecasts.length > 1
? html`<mwc-tab-bar
.activeIndex=${supportedForecasts.findIndex(
(item) => item === this._forecastType
)}
@MDCTabBar:activated=${this._handleForecastTypeChanged}
>
${supportedForecasts.map(
(forecastType) =>
html`<mwc-tab
.label=${this.hass!.localize(
`ui.card.weather.${forecastType}`
)}
></mwc-tab>`
)}
</mwc-tab-bar>`
: nothing}
${forecast.map((item) => ${forecast.map((item) =>
this._showValue(item.templow) || this._showValue(item.temperature) this._showValue(item.templow) || this._showValue(item.temperature)
? html`<div class="flex"> ? html`<div class="flex">
@@ -252,7 +288,8 @@ class MoreInfoWeather extends LitElement {
${this._showValue(item.templow) ${this._showValue(item.templow)
? this.hass.formatEntityAttributeValue( ? this.hass.formatEntityAttributeValue(
this.stateObj!, this.stateObj!,
"templow" "templow",
item.templow
) )
: hourly : hourly
? "" ? ""
@@ -262,7 +299,8 @@ class MoreInfoWeather extends LitElement {
${this._showValue(item.temperature) ${this._showValue(item.temperature)
? this.hass.formatEntityAttributeValue( ? this.hass.formatEntityAttributeValue(
this.stateObj!, this.stateObj!,
"temperature" "temperature",
item.temperature
) )
: "—"} : "—"}
</div> </div>
@@ -281,12 +319,23 @@ class MoreInfoWeather extends LitElement {
`; `;
} }
private _handleForecastTypeChanged(ev: CustomEvent): void {
this._forecastType = this._supportedForecasts(this.stateObj!)[
ev.detail.index
];
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-svg-icon { ha-svg-icon {
color: var(--paper-item-icon-color); color: var(--paper-item-icon-color);
margin-left: 8px; margin-left: 8px;
} }
mwc-tab-bar {
margin-bottom: 4px;
}
.section { .section {
margin: 16px 0 8px 0; margin: 16px 0 8px 0;
font-size: 1.2em; font-size: 1.2em;

View File

@@ -10,8 +10,8 @@ import {
mdiPencilOutline, mdiPencilOutline,
} from "@mdi/js"; } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing, PropertyValues } from "lit"; import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { cache } from "lit/directives/cache"; import { cache } from "lit/directives/cache";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@@ -38,15 +38,17 @@ import { haStyleDialog } from "../../resources/styles";
import "../../state-summary/state-card-content"; import "../../state-summary/state-card-content";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { import {
computeShowHistoryComponent,
computeShowLogBookComponent,
DOMAINS_WITH_MORE_INFO, DOMAINS_WITH_MORE_INFO,
EDITABLE_DOMAINS_WITH_ID, EDITABLE_DOMAINS_WITH_ID,
EDITABLE_DOMAINS_WITH_UNIQUE_ID, EDITABLE_DOMAINS_WITH_UNIQUE_ID,
computeShowHistoryComponent,
computeShowLogBookComponent,
} from "./const"; } from "./const";
import "./controls/more-info-default"; import "./controls/more-info-default";
import "./ha-more-info-history-and-logbook"; import "./ha-more-info-history-and-logbook";
import type { MoreInfoHistoryAndLogbook } from "./ha-more-info-history-and-logbook";
import "./ha-more-info-info"; import "./ha-more-info-info";
import type { MoreInfoInfo } from "./ha-more-info-info";
import "./ha-more-info-settings"; import "./ha-more-info-settings";
import "./more-info-content"; import "./more-info-content";
@@ -91,6 +93,9 @@ export class MoreInfoDialog extends LitElement {
@state() private _infoEditMode = false; @state() private _infoEditMode = false;
@query("ha-more-info-info, ha-more-info-history-and-logbook")
private _history?: MoreInfoInfo | MoreInfoHistoryAndLogbook;
public showDialog(params: MoreInfoDialogParams) { public showDialog(params: MoreInfoDialogParams) {
this._entityId = params.entityId; this._entityId = params.entityId;
if (!this._entityId) { if (!this._entityId) {
@@ -263,6 +268,7 @@ export class MoreInfoDialog extends LitElement {
<ha-dialog <ha-dialog
open open
@closed=${this.closeDialog} @closed=${this.closeDialog}
@opened=${this._handleOpened}
.heading=${title} .heading=${title}
hideActions hideActions
flexContent flexContent
@@ -485,6 +491,10 @@ export class MoreInfoDialog extends LitElement {
this.large = !this.large; this.large = !this.large;
} }
private _handleOpened() {
this._history?.resize({ aspectRatio: 2 });
}
static get styles() { static get styles() {
return [ return [
haStyleDialog, haStyleDialog,

View File

@@ -1,11 +1,13 @@
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { ChartResizeOptions } from "../../components/chart/ha-chart-base";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { import {
computeShowHistoryComponent, computeShowHistoryComponent,
computeShowLogBookComponent, computeShowLogBookComponent,
} from "./const"; } from "./const";
import "./ha-more-info-history"; import "./ha-more-info-history";
import type { MoreInfoHistory } from "./ha-more-info-history";
import "./ha-more-info-logbook"; import "./ha-more-info-logbook";
@customElement("ha-more-info-history-and-logbook") @customElement("ha-more-info-history-and-logbook")
@@ -14,6 +16,13 @@ export class MoreInfoHistoryAndLogbook extends LitElement {
@property() public entityId!: string; @property() public entityId!: string;
@query("ha-more-info-history")
private _history?: MoreInfoHistory;
public resize(options?: ChartResizeOptions) {
this._history?.resize(options);
}
protected render() { protected render() {
return html` return html`
${computeShowHistoryComponent(this.hass, this.entityId) ${computeShowHistoryComponent(this.hass, this.entityId)

View File

@@ -1,11 +1,12 @@
import { startOfYesterday, subHours } from "date-fns/esm"; import { startOfYesterday, subHours } from "date-fns/esm";
import { css, html, LitElement, PropertyValues, nothing } from "lit"; import { css, html, LitElement, PropertyValues, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { createSearchParam } from "../../common/url/search-params"; import { createSearchParam } from "../../common/url/search-params";
import "../../components/chart/state-history-charts"; import "../../components/chart/state-history-charts";
import type { StateHistoryCharts } from "../../components/chart/state-history-charts";
import "../../components/chart/statistics-chart"; import "../../components/chart/statistics-chart";
import { import {
computeHistory, computeHistory,
@@ -20,6 +21,8 @@ import {
StatisticsTypes, StatisticsTypes,
} from "../../data/recorder"; } from "../../data/recorder";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import type { StatisticsChart } from "../../components/chart/statistics-chart";
import { ChartResizeOptions } from "../../components/chart/ha-chart-base";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@@ -51,12 +54,22 @@ export class MoreInfoHistory extends LitElement {
private _metadata?: Record<string, StatisticsMetaData>; private _metadata?: Record<string, StatisticsMetaData>;
@query("statistics-chart, state-history-charts") private _chart?:
| StateHistoryCharts
| StatisticsChart;
public resize = (options?: ChartResizeOptions): void => {
if (this._chart) {
this._chart.resize(options);
}
};
protected render() { protected render() {
if (!this.entityId) { if (!this.entityId) {
return nothing; return nothing;
} }
return html` ${isComponentLoaded(this.hass, "history") return html`${isComponentLoaded(this.hass, "history")
? html`<div class="header"> ? html`<div class="header">
<div class="title"> <div class="title">
${this.hass.localize("ui.dialogs.more_info_control.history")} ${this.hass.localize("ui.dialogs.more_info_control.history")}

View File

@@ -1,7 +1,8 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { ChartResizeOptions } from "../../components/chart/ha-chart-base";
import { ExtEntityRegistryEntry } from "../../data/entity_registry"; import { ExtEntityRegistryEntry } from "../../data/entity_registry";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { import {
@@ -12,6 +13,7 @@ import {
DOMAINS_WITH_MORE_INFO, DOMAINS_WITH_MORE_INFO,
} from "./const"; } from "./const";
import "./ha-more-info-history"; import "./ha-more-info-history";
import type { MoreInfoHistory } from "./ha-more-info-history";
import "./ha-more-info-logbook"; import "./ha-more-info-logbook";
import "./more-info-content"; import "./more-info-content";
@@ -25,6 +27,13 @@ export class MoreInfoInfo extends LitElement {
@property({ attribute: false }) public editMode?: boolean; @property({ attribute: false }) public editMode?: boolean;
@query("ha-more-info-history")
private _history?: MoreInfoHistory;
public resize(options?: ChartResizeOptions) {
this._history?.resize(options);
}
protected render() { protected render() {
const entityId = this.entityId; const entityId = this.entityId;
const stateObj = this.hass.states[entityId] as HassEntity | undefined; const stateObj = this.hass.states[entityId] as HassEntity | undefined;

View File

@@ -1,5 +1,3 @@
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,

View File

@@ -2,7 +2,6 @@ import "@material/mwc-button";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import { PersitentNotificationEntity } from "../../data/persistent_notification"; import { PersitentNotificationEntity } from "../../data/persistent_notification";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@@ -33,15 +32,9 @@ export class HuiConfiguratorNotificationItem extends LitElement {
)} )}
</div> </div>
<mwc-button slot="actions" @click=${this._handleClick} <mwc-button slot="actions" @click=${this._handleClick}>
>${computeStateDisplay( ${this.hass.formatEntityState(this.notification)}
this.hass.localize, </mwc-button>
this.notification,
this.hass.locale,
this.hass.config,
this.hass.entities
)}</mwc-button
>
</notification-item-template> </notification-item-template>
`; `;
} }

File diff suppressed because one or more lines are too long

View File

@@ -1,103 +0,0 @@
/* eslint-plugin-disable lit */
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/*
This code is copied from app-header-layout.
'fullbleed' support is removed as Home Assisstant doesn't use it.
transform: translate(0) is added.
*/
/*
FIXME(polymer-modulizer): the above comments were extracted
from HTML and may be out of place here. Review them and
then delete this comment!
*/
import "@polymer/app-layout/app-header-layout/app-header-layout";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import "@polymer/polymer/polymer-element";
class HaAppLayout extends customElements.get("app-header-layout") {
static get template() {
return html`
<style>
:host {
display: block;
/**
* Force app-header-layout to have its own stacking context so that its parent can
* control the stacking of it relative to other elements (e.g. app-drawer-layout).
* This could be done using \`isolation: isolate\`, but that's not well supported
* across browsers.
*/
position: relative;
z-index: 0;
}
#wrapper ::slotted([slot="header"]) {
@apply --layout-fixed-top;
z-index: 1;
}
#wrapper.initializing ::slotted([slot="header"]) {
position: relative;
}
:host([has-scrolling-region]) {
height: 100%;
}
:host([has-scrolling-region]) #wrapper ::slotted([slot="header"]) {
position: absolute;
}
:host([has-scrolling-region])
#wrapper.initializing
::slotted([slot="header"]) {
position: relative;
}
:host([has-scrolling-region]) #wrapper #contentContainer {
@apply --layout-fit;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
:host([has-scrolling-region]) #wrapper.initializing #contentContainer {
position: relative;
}
#contentContainer {
/* Create a stacking context here so that all children appear below the header. */
position: relative;
z-index: 0;
/* Using 'transform' will cause 'position: fixed' elements to behave like
'position: absolute' relative to this element. */
transform: translate(0);
margin-left: env(safe-area-inset-left);
margin-right: env(safe-area-inset-right);
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
@media print {
:host([has-scrolling-region]) #wrapper #contentContainer {
overflow-y: visible;
}
}
</style>
<div id="wrapper" class="initializing">
<slot id="headerSlot" name="header"></slot>
<div id="contentContainer"><slot></slot></div>
<slot id="fab" name="fab"></slot>
</div>
`;
}
}
customElements.define("ha-app-layout", HaAppLayout);

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