Compare commits

...
Author SHA1 Message Date
Wendelin 188e82fa02 Merge branch 'dev' of github.com:home-assistant/frontend into user-siderbar 2025-02-24 09:58:13 +01:00
Joakim SørensenandGitHub a438fc5e41 Add connection check and dialog with results for cloud login (#24301) 2025-02-24 09:37:17 +01:00
karwostsandGitHub 783132ae46 Fix solar order in compare stack for usage graph (#24360)
* Fix solar order in compare stack for usage graph

* remove accidental commit
2025-02-24 09:08:55 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
680d81001c Update rspack monorepo to v1.2.5 (#24353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 09:00:19 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a917383d7a Bump actions/cache from 4.2.0 to 4.2.1 (#24366)
Bumps [actions/cache](https://github.com/actions/cache) from 4.2.0 to 4.2.1.
- [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/v4.2.0...v4.2.1)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 08:59:27 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
455a6761cd Bump actions/upload-artifact from 4.6.0 to 4.6.1 (#24365)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.0...v4.6.1)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 08:58:42 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
acf42d7637 Update dependency globals to v16 (#24359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 08:56:48 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3857c7321a Update dependency eslint-plugin-wc to v2.2.1 (#24362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 07:08:27 +01:00
puddlyandGitHub 5eec814988 Hide hardware integrations from the "add integration" dialog (#24345) 2025-02-22 08:43:18 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
edd37565a6 Update vitest monorepo to v3.0.6 (#24344)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 19:03:37 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
fb3f779121 Update rspack monorepo to v1.2.4 (#24343)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 19:03:00 +01:00
WendelinandGitHub 4d7634ac67 Landing-page: ping supervisor before get network infos (#24330)
* Ping supervisor before get network infos

* Rename supervisor proxy prefix
2025-02-21 08:14:10 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Simon Lamon
ba5c1133c6 Lock file maintenance (#24306)
* Lock file maintenance

* Bump codemirror view to 6.36.3

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Lamon <[email protected]>
2025-02-20 19:36:32 +00:00
WendelinandGitHub 0a05dd8f71 Add more tests for common/entity (#24336)
* Use substring instead of deprecated substr

* Add more common entity tests
2025-02-20 20:11:53 +01:00
400106ec09 Adjust WebSocket ping timeout to 15 seconds (#24339)
* Adjust WebSocket ping timeout to 15 seconds

5 seconds was too low to prevent the UI from reloading
when connecting the WebSocket during startup or on
a high latancy connection

This problem presented as the UI reloading over
and over again because it could never respond
to the ping in time on high latancy connections.

At startup it usually only did this once so it
went unnoticed in most cases.

This ping was added in #18934

* Update connection-mixin.ts

Co-authored-by: Jan-Philipp Benecke <[email protected]>

---------

Co-authored-by: Jan-Philipp Benecke <[email protected]>
2025-02-20 20:09:51 +01:00
Jan-Philipp BeneckeandGitHub a7a4194e09 Add tile card feature for counter actions (#24340)
* Add tile card feature for counter actions

* Format

* Change icon

* Disable buttons when hit limit

* Change increment/decrement icons
2025-02-20 19:09:44 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
0bd7d27c57 Update dependency @lokalise/node-api to v13.2.0 (#24335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 16:14:54 +01:00
Jan-Philipp BeneckeandGitHub 8175e45921 Rename switch-toggle feature to toggle and improve (#24333)
* Rename `switch-toggle` feature to `toggle` and improve

* Format
2025-02-20 14:51:49 +01:00
Jan-Philipp BeneckeandGitHub cae36b393b Focus alarm control panel PIN input on wider screens (#24324)
* Focus alarm control panel PIN input on wider screens

* Also apply on textfield
2025-02-20 15:20:28 +02:00
Paul BotteinandGitHub f84ad92356 Extract saving card config from card editor (#24319)
* Extract saving card config from card editor

* Await

* Add try/catch

* Remove unused translations

* Remove duration
2025-02-20 12:27:39 +01:00
WendelinandGitHub fb1ee2ed1d Remove toggles from ha-icon-button (#24331) 2025-02-20 12:14:40 +01:00
Paul BotteinandGitHub 9073282174 Add text only style to markdown card (#24329) 2025-02-20 11:40:39 +01:00
Jan-Philipp BeneckeandGitHub 91bd5cba08 Add switch toggle feature to tile card (#24325)
* Add tile switch toggle feature

* Remove _currentState
2025-02-20 10:16:14 +02:00
karwostsandGitHub a68bdbfe08 Fix siren advanced controls (#24318) 2025-02-20 08:50:00 +01:00
Jan-Philipp BeneckeandGitHub f3d614b0d3 Make quick bar more keyboard accessible (#24321) 2025-02-20 08:44:49 +01:00
karwostsandGitHub f3c9e4a4a0 Fix catching errors in alarm-control-panel more-info (#24328) 2025-02-20 08:42:17 +01:00
karwostsandGitHub d22a82c4a6 Teardown and rebuild element editor when switching stack cards (#24065) 2025-02-20 07:57:34 +01:00
Jan-Philipp BeneckeandGitHub 5cddc6e5c6 Decrease max cluster radius (#24322) 2025-02-19 21:34:49 +02:00
Jan-Philipp BeneckeandGitHub c5c067ef19 Create copyable textfield component (#24247) 2025-02-19 15:56:29 +01:00
Paul BotteinandGitHub 694bb3088c Improve margin for inline tile card feature (#24316) 2025-02-19 16:07:27 +02:00
Petar PetrovandGitHub ad487470fd Enable downsampling in echarts (#24311)
* Enable downsampling in echarts

* remove unneeded symbol set
2025-02-19 16:05:32 +02:00
Wendelin 2fba41b8ca Update translations 2025-02-19 14:06:34 +01:00
WendelinandGitHub 2801d071ba Fix custom retention label (#24304) 2025-02-19 10:32:41 +01:00
WendelinandGitHub 71b65f208f Fix hassio backup restore url (#24313) 2025-02-19 10:32:15 +01:00
Paul BotteinandGitHub ab4efb7412 Fix cursor jump in light color pickers (#24312) 2025-02-19 10:30:24 +01:00
Logan RosenandGitHub c7a46ec25b Improve ESLint config (#24290)
* Improve ESLint config
2025-02-18 17:30:36 +00:00
Wendelin 25a14c87a5 Delay skeleton loading to prevent flickering 2025-02-18 15:18:53 +01:00
Jan-Philipp BeneckeandGitHub 83d4a408f6 Improve large maps with marker clustering (#24244)
* Improve large maps with marker clustering

* Pin leaflet.markercluster

* Remove custom icon

* Display whether marker are clustered or not
2025-02-18 15:45:39 +02:00
Bram KragtenandGitHub 06932d1479 Prevent navigate when opening voice flow (#24300)
prevent navigate when opening voice flow
2025-02-18 14:27:59 +01:00
WendelinandGitHub 24211d5f25 Fix backup forever retention settings (#24299)
Fix forever retention settings
2025-02-18 14:05:04 +01:00
d387f19a31 Backup tweaks (#24165)
* Backup tweaks

* Show progress in fab

* Revert unused changes

---------

Co-authored-by: Wendelin <[email protected]>
2025-02-18 15:02:53 +02:00
WendelinandGitHub 347ee2a4c3 Improve-dev-container (#24296)
* Add gh cli to dev container

* Add develop and serve vscode task
2025-02-18 11:51:56 +02:00
karwostsandGitHub 1363884773 Fix error handling/flickering in markdown card (#24280) 2025-02-18 08:16:10 +01:00
Adam KaposandGitHub 0256da511d Fix theme2hex with custom theme colors (#24282) 2025-02-18 08:04:46 +01:00
Petar PetrovandGitHub c52217c1ce Make part of the chart rendering async for large datasets (#24260) 2025-02-18 07:57:07 +01:00
karwostsandGitHub cdd17eed2e Fix untracked energy rendering at the base of the bar stack (#24288) 2025-02-18 06:36:53 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4546c6f624 Update babel monorepo to v7.26.9 (#24278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 06:35:47 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2c34760204 Update vaadinWebComponents monorepo to v24.6.5 (#24279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 06:35:25 +01:00
0b64861297 Add inline features position for tile card (#24199)
* Add side features position for tile card

* Add translations

* Rename to inline

* Simplify editor with 2 dropdowns

* Use 50% width

* Update src/translations/en.json

Co-authored-by: Wendelin <[email protected]>

---------

Co-authored-by: Wendelin <[email protected]>
2025-02-18 06:33:17 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
94a5e737cc Update dependency @lit-labs/observers to v2.0.5 (#24286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 05:32:41 +00:00
Wendelin 18b2360e46 Fix memoize function 2025-02-18 06:29:17 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
05163588fc Update dependency @lit-labs/virtualizer to v2.1.0 (#24287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 06:21:23 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
ee64536862 Update dependency @lit-labs/motion to v1.0.8 (#24289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 06:20:42 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
695a6a506e Update octokit monorepo (#24292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 06:20:22 +01:00
Wendelin c1a214d1af Update code format 2025-02-17 15:56:23 +01:00
Wendelin 6123d932e1 Add loading skeleton 2025-02-17 15:49:05 +01:00
Wendelin a3dcf77f2a Add profile settings for device sidebar 2025-02-17 15:16:34 +01:00
Wendelin 7d7f8a9bc2 Use user preferences for sidebar 2025-02-17 12:56:20 +01:00
Paul BotteinandGitHub 3ee3cfa6cb Add cache for markdown card and markdown element (#24217)
* Add cache for markdown card and markdown element

* Rename to expiration

* Only use cache logic for markdown card

* Add tests

* Improve tests
2025-02-17 09:01:44 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
00d0cb7afa Update dependency @octokit/auth-oauth-device to v7.1.3 (#24273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 08:39:01 +01:00
3ae34403bd Fix duplicate id in energy-devices-detail-graph-card (#24261)
* Fix duplicate id in energy-devices-detail-graph-card

* address compare

* Update src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts

Co-authored-by: Petar Petrov <[email protected]>

* prettier

---------

Co-authored-by: Petar Petrov <[email protected]>
2025-02-16 13:06:01 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1434966170 Update dependency globals to v15.15.0 (#24262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-16 12:36:03 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8dd70f7017 Update dependency @codemirror/autocomplete to v6.18.6 (#24256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-16 12:35:58 +01:00
karwostsandGitHub 84a0289e1b Use ha-md-button-menu in automation triggers/conditions (#24258) 2025-02-16 12:35:49 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a25e1d3f7f Update CodeMirror (#24255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-15 16:53:21 +01:00
libe.netandGitHub f53ac41eee Add timespans to history and energy (#23362)
* add Last 24h, 30d, 1y and overflow

* added Energy-Dashboard

* mobile style css

* added yesterday and min-height; changed overflow; new timespans to end;

* conflict resolve trial

* changed energy timespan order

* min for logbook

* seperated overflow calc for energy and logbook / history

* rename to header-position

* prettier format

* date-fns types

* added 1h, 12 h, 7d and removed 365d for history, added 7d to energy

* remove 7d for energy

* use calcdate and for energy whole hours / days / months

* fix calc
2025-02-15 09:52:32 +01:00
karwostsandGitHub b9acd40b0f Add zones to state picker for person/device_tracker (#24201)
* Add zones to state picker for person/device_tracker

* not for attributes
2025-02-14 22:44:05 +01:00
ildar170975andGitHub 7524dc8709 Settings -> Helpers: make "Editable" columns sortable (#23976)
* "Editable" in "Helpers"

* prettier
2025-02-14 22:27:28 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
cbedf62c39 Update dependency @codemirror/autocomplete to v6.18.5 (#24249)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 22:16:15 +01:00
WendelinandGitHub 63a98155cd Add more unit tests for common/entity (#24182)
* Add new entity tests

* Improve canToggleDomain test
2025-02-14 21:55:23 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Simon Lamon
7369b7e0d5 Update dependency prettier to v3.5.1 (#24203)
* Update dependency prettier to v3.5.0

* Prettier 3.5.1

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Lamon <[email protected]>
2025-02-14 20:27:21 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
922abafabf Bump @octokit/plugin-paginate-rest from 11.4.0 to 11.4.2 (#24245)
Bumps [@octokit/plugin-paginate-rest](https://github.com/octokit/plugin-paginate-rest.js) from 11.4.0 to 11.4.2.
- [Release notes](https://github.com/octokit/plugin-paginate-rest.js/releases)
- [Commits](https://github.com/octokit/plugin-paginate-rest.js/compare/v11.4.0...v11.4.2)

---
updated-dependencies:
- dependency-name: "@octokit/plugin-paginate-rest"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-14 21:08:23 +01:00
WendelinandGitHub f1bb4a5694 Add title attribute to data-table column header (#24231) 2025-02-14 21:07:48 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e0b9cb8ccb Bump @octokit/request-error from 6.1.6 to 6.1.7 (#24243)
Bumps [@octokit/request-error](https://github.com/octokit/request-error.js) from 6.1.6 to 6.1.7.
- [Release notes](https://github.com/octokit/request-error.js/releases)
- [Commits](https://github.com/octokit/request-error.js/compare/v6.1.6...v6.1.7)

---
updated-dependencies:
- dependency-name: "@octokit/request-error"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-14 19:59:24 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
06f27650da Bump @octokit/request from 9.1.4 to 9.2.1 (#24242)
Bumps [@octokit/request](https://github.com/octokit/request.js) from 9.1.4 to 9.2.1.
- [Release notes](https://github.com/octokit/request.js/releases)
- [Commits](https://github.com/octokit/request.js/compare/v9.1.4...v9.2.1)

---
updated-dependencies:
- dependency-name: "@octokit/request"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-14 20:38:18 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a772eaffd7 Update dependency eslint to v9.20.1 (#24241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 19:23:05 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c39be4a9b8 Bump @octokit/endpoint from 10.1.1 to 10.1.3 (#24239)
Bumps [@octokit/endpoint](https://github.com/octokit/endpoint.js) from 10.1.1 to 10.1.3.
- [Release notes](https://github.com/octokit/endpoint.js/releases)
- [Commits](https://github.com/octokit/endpoint.js/compare/v10.1.1...v10.1.3)

---
updated-dependencies:
- dependency-name: "@octokit/endpoint"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-14 20:12:57 +01:00
Petar PetrovandGitHub 0abccb88d6 Fix inclusion dialog in ZwaveJS panel (#24234) 2025-02-14 13:32:09 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5dc5879773 Update rspack monorepo to v1.2.3 (#24235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 13:10:38 +01:00
karwostsandGitHub 41df7a3f4a Keyboard accessibility for automation-action-row (convert M2->M3) (#24121) 2025-02-14 13:02:16 +01:00
Petar PetrovandGitHub 920ec035c5 Fix endTime of statistics-chart (#24233) 2025-02-14 12:53:48 +01:00
Petar PetrovandGitHub 043e8d6e2e Optimize chart performance (#24215)
* Stop listening to chart scroll events to improve performance

* only set visualmap when needed

* Reduce statistics detail for long periods

* reduce calls to `setOption`

* tweak zoom modifier code

* always replace series

* revert statistics detail change
2025-02-14 12:46:55 +01:00
ildar170975andGitHub d8e36894a0 Fix for "Increase generic entity row touch target (4): iOS troubles (#24224)
restoring pre-2025.2 height
2025-02-14 09:45:56 +01:00
ildar170975andGitHub 65b6a3c6a3 developer-tools-statistics: fix height of ha-data-table to avoid a double scrollbar (#24226)
height fix
2025-02-14 09:30:46 +01:00
ildar170975andGitHub b16f82cedb Settings->Entities: set width for "Status" (#23975)
* min-width for "Status"

* max-width for "Status"
2025-02-14 09:17:58 +01:00
Paul BotteinandGitHub 02deeb4ce7 Increase target zone for tile card icon click (#24219) 2025-02-14 08:52:13 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
0c6651c2c2 Update typescript-eslint monorepo to v8.24.0 (#24230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 08:47:20 +02:00
Paulus SchoutsenandGitHub abbf56db1d Fix config flow URLs linking to device (#24223) 2025-02-13 13:47:03 +00:00
Norbert RittelandGitHub bc0cc8b387 Fix sentence-casing of running_parallel state for scripts (#24218)
Fix sentence-casing of running_parallel state of scripts
2025-02-13 12:54:52 +01:00
Abílio CostaandGitHub b66f41db7d Improve last backup status string (#24206) 2025-02-13 07:50:59 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
05fbe204c5 Update dependency ua-parser-js to v2.0.2 (#24205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-13 07:47:40 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
ee199fbbc0 Update dependency marked to v15.0.7 (#24210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-13 07:46:37 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
56ab29da81 Update formatjs monorepo (#24195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-12 18:59:29 +01:00
Paul BotteinandGitHub 10abaa538d Improve tile card interactions (#24175)
* Use none instead of more info for icon

* Improve tile icon accessibility

* Remove background shape for tile card icon when no action

* Add hover opacity

* Fix wrong type

* Remove padding around icon and increase hover opacity
2025-02-12 10:49:31 +01:00
ildar170975andGitHub f25dac7f68 Settings -> Automations: show a title for "State" column (#23977)
show a title for "State" column
2025-02-12 09:46:40 +01:00
karwostsandGitHub 99065a689f Retry subscribing to weather forecast if it fails (#24188) 2025-02-12 08:39:46 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
ac88d5993a Update dependency @lokalise/node-api to v13.1.0 (#24191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 23:04:02 +01:00
Paul BotteinandGitHub b09ce45d31 Display hold and double tap actions in tile card editor if they are set (#24178) 2025-02-11 16:45:48 +01:00
Paul BotteinandGitHub 78e2809fe7 Fix default value for color in entity badge editor (#24186) 2025-02-11 16:14:04 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a631bf9854 Update babel monorepo to v7.26.8 (#24183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 13:53:07 +01:00
ildar170975andGitHub 1349c8520c developer-tools-template: allow "select all" for "rendered" (2) (#24171)
removed a harmful user-select
2025-02-11 13:26:46 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
6d1a55cc3a Update vaadinWebComponents monorepo to v24.6.4 (#24153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 09:48:10 +01:00
WendelinandGitHub 23a9ae6835 Onboarding restore use error code (#24172)
Use error code for incorrect password
2025-02-11 09:47:18 +01:00
WendelinandGitHub dbd1e928de Make restore button destructive (#24173) 2025-02-11 09:37:56 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
e86ad21ce2 Update dependency eslint to v9.20.0 (#24169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 09:12:21 +01:00
Bram KragtenandGitHub 0d97afb3f2 Add base support for sub entries (#23160)
* Add base support for sub entries

* add demo types

* fix translations

* Use sub entry name when deleting

* Update show-dialog-sub-config-flow.ts

* adjust for multiple sub types

* WIP, not functional

* add subentry_type

* rename to supported_subentry_types

* config_subentries -> config_entries_subentries

* Add localized sub flow title

* use Record

* rename

* more rename
2025-02-10 21:24:05 +01:00
03a415beff Onboarding restore use core api (#23920)
* Fix type issues

* Extract backup-upload

* Add onboarding upload section

* Extract and use ha-backup-details

* Implement backup details and restore

* remove unused hassio onboarding calls

* Require hass in dialog-hassio-backup

* Add restore view

* Add formatDateTime without locale and config

* Add restore status

* Fix prettier

* Fix styles of backup details

* Remove unused localize

* Fix onboarding restore translations

* Hide data-picker on core only instance

* Split uploadBackup into 2 separate funcs

* Add formatDateTimeWithBrowserDefaults

* Fix selected data for core only

* Show error reasons on status page

* Use new backup info agents

* Add mem function for formatDateTimeWithBrowserDefaults

* Fix overflow on mobile

* Handle errors when in hassio mode

* Fix cancel restore texts

* Fix hassio localize type issue

* Remove unused onboarding localize in hassio backup restore

* improve format_date_time

* Fix tests

* Fix and simplify backup upload issues

* Use foreach instead of reduce

* Fix attributes, unused styles and properties

* Simplify supervisor warning

* Fix language type issues

* Fix ha-backup-data-picker

* Improve backup-details-restore

* Fix onboarding-restore issues

* Improve loadBackupInfo

* Revert uploadBackup changes

* Improve cancel restore

* Use destructive

* Update src/panels/config/backup/dialogs/dialog-upload-backup.ts

Co-authored-by: Bram Kragten <[email protected]>

* Show backup type not at onboarding

* Only show backup type in correct translationPanel

* Fix quotes

---------

Co-authored-by: Bram Kragten <[email protected]>
2025-02-10 16:40:08 +01:00
Paulus SchoutsenandGitHub 44cc75afbc Require opt-in config flow navigateToResult (#24120) 2025-02-10 15:58:10 +01:00
Petar PetrovandGitHub 748642a8d6 Limit max label width of hui-energy-devices-graph-card (#24152) 2025-02-10 15:54:20 +01:00
Petar PetrovandGitHub 3d5c65d652 Bring back energy usage graph order (#24156) 2025-02-10 15:53:36 +01:00
Paul BotteinandGitHub a26bf80b13 Fix section border radius (#24159) 2025-02-10 15:53:05 +01:00
Petar PetrovandGitHub 497c6c35f1 Fix device energy card with max_devices (#24150) 2025-02-10 10:12:42 +01:00
Petar PetrovandGitHub b0b06a2787 Round log scale limits (#24151) 2025-02-10 10:12:12 +01:00
Paulus SchoutsenandGitHub f3d55447ca Add support for intent-progress assist events (#24143) 2025-02-09 23:36:56 -05:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1b3d4b77d3 Update dependency ua-parser-js to v2.0.1 (#24125)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-08 21:14:11 +02:00
Paulus SchoutsenandGitHub 6ec4041c4c Navigate to newly created config entry (#24109) 2025-02-07 08:32:21 +02:00
karwostsandGitHub d919e8d333 Integrate Statistic Card with Energy Date Picker (#23794)
* Support energy-date-selection for statistic card

* reuse period key
2025-02-07 08:29:28 +02:00
karwostsandGitHub af7bb85667 Stack solar forecasts (#24113) 2025-02-07 06:44:39 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
9061e2039b Update dependency @vitest/coverage-v8 to v3.0.5 (#24106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 19:40:41 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
906e6f4a88 Update dependency @codemirror/state to v6.5.2 (#24105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 19:26:48 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
73fbe9a69d Update typescript-eslint monorepo to v8.23.0 (#24108)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 19:26:16 +01:00
Petar PetrovandGitHub 2a0f69a629 Fix energy dashboard data formatting (#24101) 2025-02-06 12:38:22 +01:00
Petar PetrovandGitHub 9411a77f14 Set charts font to Roboto (#24097) 2025-02-06 13:16:35 +02:00
Petar PetrovandGitHub de3bf2e088 Show energy-self-sufficiency-gauge card without grid return (#24098)
Show energy-self-sufficiency-gauge card if solar is defined
2025-02-06 10:07:06 +00:00
Brynley McDonaldandGitHub 16181b48ae Add Mastodon and Bluesky to help tip (#24099)
Add Mastodon and Bluesky to socials tip
2025-02-06 10:59:58 +01:00
Curt GrimesandGitHub 8682debe61 Fix punctuation in some toast and warning messages (#24093)
There are some toast messages that are comma splices (two independent
clauses joined by a comma), which is not correct grammar. This commit
fixes the punctuation in these messages.
2025-02-06 09:37:54 +01:00
Petar PetrovandGitHub bdbc9bc1b4 Reduce padding in energy charts and align unit (#24095) 2025-02-06 09:36:58 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Petar Petrov
79b9f8d083 Update dependency barcode-detector to v3 (#24015)
* Update dependency barcode-detector to v3

* fix breaking changes

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <[email protected]>
2025-02-06 07:53:26 +00:00
WendelinandGitHub 3918194d2d Add network adapter translations (#24096) 2025-02-06 09:49:05 +02:00
Charles GarwoodandGitHub e9fef1f873 Update more-info dialog layout for weather entities (#22818)
* Update weather more-info style

* Cleanup unused var

* Use badges for attributes

* Remove unnecessary flex class

* CSS cleanup

* Wrap badges

* Revert "Cleanup unused var"

This reverts commit 89ab0f6ad05e1e669b84e69f4c263e3d302794f2.

* Revert badges for attributes

* Scroll long forecasts

* Use nothing instead of empty strings

* Cleanup
2025-02-06 07:11:30 +01:00
Jan-Philipp BeneckeandGitHub 35face602b Fix area registry dialog field (#24090) 2025-02-05 18:33:00 +01:00
Bram Kragten 803ac496f6 Merge branch 'rc' into dev 2025-02-05 16:32:25 +01:00
Bram Kragten f1173dd84b Bumped version to 20250205.0 2025-02-05 16:27:17 +01:00
Petar PetrovandBram Kragten 44dcca9923 Fix chart height (#24028) 2025-02-05 16:25:44 +01:00
Bram Kragten bd74d39dd8 Use max of width and actualBoundingBox to get text width (#24085) 2025-02-05 16:20:22 +01:00
Petar PetrovandBram Kragten 172d6c3079 Disable chart update animation (#24084) 2025-02-05 16:20:21 +01:00
Bram Kragten 56539e8065 Charts: set tooltip triggerOn to click on mobile (#24083)
set tooltip triggerOn to click on mobile
2025-02-05 16:20:20 +01:00
Bram Kragten 8f6867f142 Chart: Add tooltip styling to theme (#24082) 2025-02-05 16:20:19 +01:00
Bram Kragten d51f8995dd Charts: add styles for legend page controls (#24081) 2025-02-05 16:20:19 +01:00
Petar PetrovandBram Kragten f2e35dc70a Fix chart preview (#24080)
* Fix chart preview

* Revert change to timeline-chart labels
2025-02-05 16:20:18 +01:00
Petar PetrovandBram Kragten 6487b9b7ea Fix device energy bar chart (#24079) 2025-02-05 16:20:17 +01:00
Bram Kragten e50b658db7 Set min height for graphs, adjust margins (#24078)
* Set min height for graphs, adjust margins

* stats + header adjustments

* set min to 200
2025-02-05 16:19:29 +01:00
Bram Kragten 6efe237639 Fix label truncated timeline chart (#24077) 2025-02-05 16:15:00 +01:00
Bram Kragten 4a94cfc05b Set list color of update more info to dialog background (#24076) 2025-02-05 16:15:00 +01:00
Bram Kragten 7cbdb1dcfd Fix condition in tracing graph (#24075) 2025-02-05 16:14:59 +01:00
Paul BotteinandBram Kragten 553bb61db7 Fix statistic chart tooltip values (#24074) 2025-02-05 16:14:58 +01:00
Petar PetrovandBram Kragten 786ff787d1 Fix spacing & colors in statistics-graph chart (#24068)
* Fix statistic chart colors

* Fix spacing in statistics-graph

* set start time based on data
2025-02-05 16:14:57 +01:00
Bram KragtenandGitHub 28b3f2970a Use max of width and actualBoundingBox to get text width (#24085) 2025-02-05 17:12:48 +02:00
Petar PetrovandGitHub 7d170a710e Disable chart update animation (#24084) 2025-02-05 15:04:03 +00:00
Bram KragtenandGitHub cc40b50675 Charts: set tooltip triggerOn to click on mobile (#24083)
set tooltip triggerOn to click on mobile
2025-02-05 15:02:43 +00:00
Bram KragtenandGitHub b6eaff46e9 Chart: Add tooltip styling to theme (#24082) 2025-02-05 16:37:13 +02:00
Bram KragtenandGitHub 674bb0d16a Set min height for graphs, adjust margins (#24078)
* Set min height for graphs, adjust margins

* stats + header adjustments

* set min to 200
2025-02-05 15:16:33 +01:00
Petar PetrovandGitHub 6ff018afc9 Fix chart preview (#24080)
* Fix chart preview

* Revert change to timeline-chart labels
2025-02-05 15:16:16 +01:00
Bram KragtenandGitHub ad48732bb7 Charts: add styles for legend page controls (#24081) 2025-02-05 16:04:14 +02:00
Bram KragtenandGitHub fef162346a Set list color of update more info to dialog background (#24076) 2025-02-05 12:48:13 +01:00
Bram KragtenandGitHub 72d208d1ac Fix label truncated timeline chart (#24077) 2025-02-05 12:47:39 +01:00
Petar PetrovandGitHub 5a8b1b0fd4 Fix device energy bar chart (#24079) 2025-02-05 12:47:21 +01:00
Bram KragtenandGitHub 4cfc651799 Fix condition in tracing graph (#24075) 2025-02-05 10:04:39 +00:00
Petar PetrovandGitHub b4a3f4cb2c Fix spacing & colors in statistics-graph chart (#24068)
* Fix statistic chart colors

* Fix spacing in statistics-graph

* set start time based on data
2025-02-05 11:22:31 +02:00
Paul BotteinandGitHub f0507a88a6 Fix statistic chart tooltip values (#24074) 2025-02-05 11:18:29 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
fe041e442d Update dependency vitest to v3.0.5 [SECURITY] (#24066)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 19:28:01 +01:00
Bram Kragten e5fea98460 Bumped version to 20250204.0 2025-02-04 18:22:43 +01:00
Petar PetrovandBram Kragten 31180e3a9e Fix energy charts with leap years (#24059)
* Fix energy charts with leap years

* handle quarters
2025-02-04 18:21:27 +01:00
Paul BotteinandBram Kragten ce0f02a45b Display unavailable backups locations (#24058)
Display anavailable backups locations
2025-02-04 18:21:27 +01:00
Paul BotteinandBram Kragten 53f090356e Improve value formatting inside backup tooltip (#24057) 2025-02-04 18:21:26 +01:00
Bram Kragten 776c4da688 Add support package download to cloud (#24051) 2025-02-04 18:21:25 +01:00
Bram Kragten 849922f7be Dont show voice wizard for voip (#24050)
dont show voice wizard for voip
2025-02-04 18:21:24 +01:00
Paul BotteinandBram Kragten a26701808f Add support for add-on update type for backups in the UI (#24044)
* Add support for add-on update type for backups in the UI

* Add type to backup detail page

* Use new model

* Fix detail page

* Fix type
2025-02-04 18:21:23 +01:00
Bram KragtenandGitHub 904ee2e418 Add support package download to cloud (#24051) 2025-02-04 18:06:41 +01:00
Paul BotteinandGitHub 11ae3a77e8 Add support for add-on update type for backups in the UI (#24044)
* Add support for add-on update type for backups in the UI

* Add type to backup detail page

* Use new model

* Fix detail page

* Fix type
2025-02-04 16:04:11 +01:00
Paul BotteinandGitHub 3a12019b64 Display unavailable backups locations (#24058)
Display anavailable backups locations
2025-02-04 14:45:38 +01:00
Bram KragtenandGitHub 6c2cf1ff60 Dont show voice wizard for voip (#24050)
dont show voice wizard for voip
2025-02-04 14:38:03 +02:00
Petar PetrovandGitHub 02ae0b5864 Fix energy charts with leap years (#24059)
* Fix energy charts with leap years

* handle quarters
2025-02-04 12:28:31 +00:00
Jan-Philipp BeneckeandGitHub 85fe2213c1 Align view mount dialog with design guidelines (#24060)
Align view mount dialog with design requirements
2025-02-04 13:25:59 +01:00
Paul BotteinandGitHub 7dbc78f1d6 Improve value formatting inside backup tooltip (#24057) 2025-02-04 13:47:24 +02:00
f965a3504f Improve weather forecast card layout (#23704)
* Improve weather forecast card layout

* fix units and improve naming

Co-authored-by: Petar Petrov <[email protected]>

---------

Co-authored-by: Petar Petrov <[email protected]>
2025-02-04 06:32:30 +00:00
karwostsandGitHub 077f5efe7e Fix menus in Todo list for Keyboard (#24054) 2025-02-04 08:30:45 +02:00
Bram Kragten ef3bea71a0 Bumped version to 20250203.0 2025-02-03 18:20:20 +01:00
Petar PetrovandBram Kragten fcf655b0ec FIx console errors in charts (#24048)
* FIx console errors in charts

* handle undefined unit_of_measurement
2025-02-03 18:20:17 +01:00
Paul BotteinandBram Kragten b263b74916 Increase margin to avoid fab overlap on backup overview page (#24047) 2025-02-03 18:20:16 +01:00
Paul BotteinandBram Kragten 0f4b6b423a Improve chart height and narrow option in grid section (#24046)
* Fix chart size in grid

* Set minimal height to 2 for history chart

* Update history chart
2025-02-03 18:20:15 +01:00
Paul BotteinandBram Kragten 72df585c5e Fix download unencrypted backup logic (#24045) 2025-02-03 18:20:15 +01:00
Petar PetrovandBram Kragten 4698a63642 Show seconds on x axis when chart is zoomed a lot (#24043)
Show seconds on x axis when charts is zoomed a lot
2025-02-03 18:20:14 +01:00
Petar PetrovandBram Kragten 6eb43a7d61 Workaround for chart size bug in editor preview (#24040) 2025-02-03 18:20:13 +01:00
Petar PetrovandBram Kragten af35b15400 Fix click action for timeline chart labels (#24039)
* Fix click action for timeline chart labels

* Use id in line charts
2025-02-03 18:20:12 +01:00
Petar PetrovandBram Kragten 0d50d2664f Fix legend in charts (#24025)
* Fix legend in line charts

* fix statistics graph legend
2025-02-03 18:20:11 +01:00
PhilippandBram Kragten ff1159402e Fix browser media player showing more info dialog (#24021) 2025-02-03 18:20:11 +01:00
Jan-Philipp BeneckeandBram Kragten f8742ae690 Display year conditionally when script was last triggered on script list (#24012)
Display year conditionally when script was last triggered in script list
2025-02-03 18:20:10 +01:00
ildar170975andBram Kragten c786d26542 Fix for "Increase generic entity row touch target (3): climate entities (#24002)
return to max-height + set vertical alignment
2025-02-03 18:20:09 +01:00
Bram Kragten 3f8ff94002 Make date period picker respect timezone settings (#23996) 2025-02-03 18:20:08 +01:00
Petar PetrovandGitHub 64a968543b FIx console errors in charts (#24048)
* FIx console errors in charts

* handle undefined unit_of_measurement
2025-02-03 18:13:35 +01:00
karwostsandGitHub aea98f702b Prioritize local image over entity_picture in picture-entity card (#24032)
* Prioritize local image over entity_picture in picture-entity

* Remove the default stub image if we switch to an entity with a picture

* minor cleanup
2025-02-03 16:01:16 +01:00
Paul BotteinandGitHub 863ff622be Increase margin to avoid fab overlap on backup overview page (#24047) 2025-02-03 16:00:08 +01:00
Petar PetrovandGitHub 730cea6646 Workaround for chart size bug in editor preview (#24040) 2025-02-03 15:59:43 +01:00
7d1f8d618a Fix more keyboard menus (devices/helpers/scenes/scripts) (#24037)
* Fix more keyboard menus (devices/helpers/scenes/scripts)

* Apply suggestions from code review

Co-authored-by: Petar Petrov <[email protected]>

* less async

---------

Co-authored-by: Petar Petrov <[email protected]>
2025-02-03 16:13:20 +02:00
Paul BotteinandGitHub 67b970fcaa Improve chart height and narrow option in grid section (#24046)
* Fix chart size in grid

* Set minimal height to 2 for history chart

* Update history chart
2025-02-03 14:12:28 +00:00
Paul Bottein 38bcdaa6f6 Revert "Fix chart size in grid"
This reverts commit 8f1389de66.
2025-02-03 14:55:17 +01:00
Paul Bottein 8f1389de66 Fix chart size in grid 2025-02-03 14:51:19 +01:00
Paul BotteinandGitHub 37ac796c8f Fix download unencrypted backup logic (#24045) 2025-02-03 14:40:49 +01:00
Petar PetrovandGitHub 716cd19d41 Show seconds on x axis when chart is zoomed a lot (#24043)
Show seconds on x axis when charts is zoomed a lot
2025-02-03 13:27:33 +01:00
Paul BotteinandGitHub 173725f011 Use ignoreDiacritics in fuse library (#24041) 2025-02-03 12:53:34 +01:00
Petar PetrovandGitHub ad561b885b Fix click action for timeline chart labels (#24039)
* Fix click action for timeline chart labels

* Use id in line charts
2025-02-03 10:36:59 +00:00
PhilippandGitHub d77bdf4ac6 Fix browser media player showing more info dialog (#24021) 2025-02-03 11:36:19 +01:00
Petar PetrovandGitHub ac3796ec31 Fix chart height (#24028) 2025-02-03 11:21:30 +01:00
Petar PetrovandGitHub 8c3fdfb6fb Fix legend in charts (#24025)
* Fix legend in line charts

* fix statistics graph legend
2025-02-03 11:09:59 +01:00
karwostsandGitHub b7c7d0b4b5 Scroll todo list if it overflows grid_layout (#24000) 2025-02-03 09:09:01 +02:00
ildar170975andGitHub 8b0e6eed3a Fix for "Increase generic entity row touch target (3): climate entities (#24002)
return to max-height + set vertical alignment
2025-02-03 09:07:11 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
603f884e8c Update dependency @types/chromecast-caf-receiver to v6.0.21 (#24038)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 08:09:55 +02:00
Bram KragtenandGitHub 97dfccf4c7 Make date period picker respect timezone settings (#23996) 2025-02-01 17:32:07 +01:00
Jan-Philipp BeneckeandGitHub fd1e31c0cc Display year conditionally when script was last triggered on script list (#24012)
Display year conditionally when script was last triggered in script list
2025-02-01 17:16:43 +01:00
Bram Kragten 1de740e7b5 Bumped version to 20250131.0 2025-01-31 18:20:12 +01:00
Bram Kragten 5abfb90b16 fix time input width (#23998) 2025-01-31 18:19:45 +01:00
Petar PetrovandBram Kragten 6b691063a8 Hide "heating" data from climate charts (#23997) 2025-01-31 18:19:44 +01:00
Paul BotteinandBram Kragten d1d746e7e6 Remove name from the chart series when using showNames = false (#23995)
* Remove name from the chart series when using showNames = false

* Remove translations
2025-01-31 18:19:43 +01:00
Petar PetrovandBram Kragten 2fcb64d4a1 Echarts: auto scale Y in log charts (#23994)
* Echarts: auto scale Y in log charts

* fix statistics chart log scale
2025-01-31 18:19:42 +01:00
Petar PetrovandBram Kragten 3769f8c7c0 Hide irrelevant errors from echarts zoom (#23992) 2025-01-31 18:19:41 +01:00
Paul BotteinandBram Kragten f0a56e75f5 Improve encrypted backup dialog (#23991)
* Improve encrypted backup dialog

* Remove unused code
2025-01-31 18:19:40 +01:00
Petar PetrovandBram Kragten 15f33e1f19 Echarts: show all series in tooltip (#23989)
* Echarts: show all series in tooltip

* fix typo

* remove duplicate tooltip entries in statistics chart

* take last valid point instead of first
2025-01-31 18:19:40 +01:00
Petar PetrovandBram Kragten 181122177b Echarts: fix Y scaling (#23988)
* Echarts: fix scaling of Y axis

* fix fit logic to only extend the limits

* handle invalid min for log scale
2025-01-31 18:19:39 +01:00
Petar PetrovandBram Kragten 684cd0f627 Fix legend resetting on zoom (#23985) 2025-01-31 18:19:38 +01:00
Paul BotteinandBram Kragten 277202e363 Use smooth line for statistic line chart (#23984)
* Use smooth line for statistic line chart

* Use same smooth options as chartjs
2025-01-31 18:19:37 +01:00
Petar PetrovandBram Kragten b388d1fd42 Fix statistics echarts with negative values (#23983)
* Fix statistics echarts with negative values

* fix border-radius of negative bar values

* revert timeline label width to previous max values
2025-01-31 18:19:37 +01:00
Paul BotteinandBram Kragten 251e6399f5 Reduce chart height to 300px (#23979) 2025-01-31 18:19:36 +01:00
karwostsandBram Kragten f44c5d7a63 Improve statistics graph axis when using energy_date_selection (#23974) 2025-01-31 18:19:35 +01:00
ildar170975andBram Kragten cae1ca52f0 Fix for "Increase generic entity row touch target (2) (#23973)
* Revert "Fix for "Increase generic entity row touch target" (#23953)"

This reverts commit 028472fc7b.

* conditional style
2025-01-31 18:19:34 +01:00
Petar PetrovandGitHub f8de2c64a5 Hide "heating" data from climate charts (#23997) 2025-01-31 17:13:13 +00:00
Bram KragtenandGitHub 34ef5be720 fix time input width (#23998) 2025-01-31 17:12:49 +00:00
Petar PetrovandGitHub 1402802031 Echarts: auto scale Y in log charts (#23994)
* Echarts: auto scale Y in log charts

* fix statistics chart log scale
2025-01-31 17:42:51 +01:00
Paul BotteinandGitHub 816989ab4d Remove name from the chart series when using showNames = false (#23995)
* Remove name from the chart series when using showNames = false

* Remove translations
2025-01-31 17:38:50 +01:00
Paul BotteinandGitHub d4497ca39c Improve encrypted backup dialog (#23991)
* Improve encrypted backup dialog

* Remove unused code
2025-01-31 16:10:43 +00:00
Petar PetrovandGitHub 6e39242ca3 Echarts: fix Y scaling (#23988)
* Echarts: fix scaling of Y axis

* fix fit logic to only extend the limits

* handle invalid min for log scale
2025-01-31 15:44:22 +01:00
Petar PetrovandGitHub 0197e32783 Echarts: show all series in tooltip (#23989)
* Echarts: show all series in tooltip

* fix typo

* remove duplicate tooltip entries in statistics chart

* take last valid point instead of first
2025-01-31 15:42:46 +01:00
Petar PetrovandGitHub 87dfed4beb Hide irrelevant errors from echarts zoom (#23992) 2025-01-31 15:37:47 +01:00
WendelinandGitHub dae991dc89 Improve develop and serve (#23990) 2025-01-31 15:50:15 +02:00
Paul BotteinandGitHub 6197e3483b Use smooth line for statistic line chart (#23984)
* Use smooth line for statistic line chart

* Use same smooth options as chartjs
2025-01-31 11:46:01 +01:00
Petar PetrovandGitHub b2a6c8bd36 Fix legend resetting on zoom (#23985) 2025-01-31 11:13:46 +01:00
Petar PetrovandGitHub 938855e13c Fix statistics echarts with negative values (#23983)
* Fix statistics echarts with negative values

* fix border-radius of negative bar values

* revert timeline label width to previous max values
2025-01-31 12:02:39 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a8712e3b8e Update vaadinWebComponents monorepo to v24.6.3 (#23981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 10:31:11 +01:00
Paul BotteinandGitHub b15b577057 Reduce chart height to 300px (#23979) 2025-01-31 10:42:51 +02:00
karwostsandGitHub 653aeae3d8 Improve statistics graph axis when using energy_date_selection (#23974) 2025-01-31 08:45:37 +02:00
ildar170975andGitHub 0aea6141ad Fix for "Increase generic entity row touch target (2) (#23973)
* Revert "Fix for "Increase generic entity row touch target" (#23953)"

This reverts commit 028472fc7b.

* conditional style
2025-01-31 08:41:38 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5243c1d871 Update typescript-eslint monorepo to v8.22.0 (#23972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 08:39:18 +02:00
Bram Kragten 9449f5ad0a Bumped version to 20250130.0 2025-01-30 18:08:13 +01:00
Paul BotteinandBram Kragten c337bc5f97 Improve backup settings display on mobile (#23967) 2025-01-30 18:07:52 +01:00
Petar PetrovandBram Kragten 6aab60cf45 Dynamically reorder energy devices (echarts) (#23966)
* Dynamically reorder energy devices (echarts)

* fix initial sorting in hui-energy-devices-detail-graph-card

* fix dynamic reordering in devices detail
2025-01-30 18:07:51 +01:00
Paul BotteinandBram Kragten 52e9bc3213 Fix backup location config not updated (#23965) 2025-01-30 18:07:50 +01:00
Paul BotteinandBram Kragten e48b2383cf Fix location icon when many locations in backup datatable (#23964)
* Fix location icon when many locations in backup datatable

* Reuse data

* Don't copy twice

* Improve naming
2025-01-30 18:07:50 +01:00
Petar PetrovandBram Kragten 002a249777 Use CSS variables to theme echarts (#23963)
* Use CSS variables to theme echarts

* fix styles
2025-01-30 18:07:49 +01:00
Paul BotteinandBram Kragten 10498ce18d Display device name in bluetooth panel (#23960) 2025-01-30 18:07:48 +01:00
WendelinandBram Kragten 6a5936b2b2 Add correct link to backup.create_automatic (#23959) 2025-01-30 18:07:47 +01:00
Norbert RittelandBram Kragten dc68aaa803 Add localizable "Actions" label to OAuth credentials picker (#23958)
* Add localizable "Actions" label to OAuth credentials picker

* Prettier
2025-01-30 18:07:46 +01:00
Paul BotteinandBram Kragten e7931ce049 Restore scroll position go back to backup settings page (#23955) 2025-01-30 18:07:46 +01:00
ildar170975andBram Kragten 59b2582fe3 Fix for "Increase generic entity row touch target" (#23953)
fix for "touch target"
2025-01-30 18:07:45 +01:00
karwostsandBram Kragten 8577b0721c Fix untracked energy in compare (#23949) 2025-01-30 18:07:44 +01:00
J. Nick KostonandBram Kragten 91319be855 Reduce size of address column on Bluetooth Advertisement monitor (#23942) 2025-01-30 18:07:43 +01:00
Simon LamonandBram Kragten 0dff538298 Backup location translations improvements (#23940)
* Backup location translations improvements

* Apply better translations
2025-01-30 18:07:42 +01:00
Simon LamonandGitHub 6ac6d9c6eb Backup location translations improvements (#23940)
* Backup location translations improvements

* Apply better translations
2025-01-30 18:06:57 +01:00
Norbert RittelandGitHub 6ba0071296 Add localizable "Actions" label to OAuth credentials picker (#23958)
* Add localizable "Actions" label to OAuth credentials picker

* Prettier
2025-01-30 18:05:18 +01:00
Paul BotteinandGitHub fef5dc4232 Fix location icon when many locations in backup datatable (#23964)
* Fix location icon when many locations in backup datatable

* Reuse data

* Don't copy twice

* Improve naming
2025-01-30 17:02:56 +00:00
Paul BotteinandGitHub ce58962dbb Fix backup location config not updated (#23965) 2025-01-30 17:43:39 +01:00
Petar PetrovandGitHub 9fb1e1d2ed Dynamically reorder energy devices (echarts) (#23966)
* Dynamically reorder energy devices (echarts)

* fix initial sorting in hui-energy-devices-detail-graph-card

* fix dynamic reordering in devices detail
2025-01-30 17:43:06 +01:00
Paul BotteinandGitHub a29544c1e6 Improve backup settings display on mobile (#23967) 2025-01-30 17:49:05 +02:00
Petar PetrovandGitHub b2b71edd04 Use CSS variables to theme echarts (#23963)
* Use CSS variables to theme echarts

* fix styles
2025-01-30 14:39:59 +01:00
ildar170975andGitHub 028472fc7b Fix for "Increase generic entity row touch target" (#23953)
fix for "touch target"
2025-01-30 13:26:11 +01:00
Paul BotteinandGitHub b056ce228b Display device name in bluetooth panel (#23960) 2025-01-30 12:36:10 +01:00
WendelinandGitHub 0cd4256c0e Add correct link to backup.create_automatic (#23959) 2025-01-30 11:05:33 +00:00
Yosi LevyandGitHub e274c5b23f Add node memory to allow commit (#23954) 2025-01-30 11:06:50 +02:00
karwostsandGitHub ea57846465 Fix untracked energy in compare (#23949) 2025-01-30 09:57:54 +01:00
Paul BotteinandGitHub 3f2e2bc659 Restore scroll position go back to backup settings page (#23955) 2025-01-30 09:56:52 +01:00
J. Nick KostonandGitHub e3f2f66206 Reduce size of address column on Bluetooth Advertisement monitor (#23942) 2025-01-29 19:01:47 +00:00
227 changed files with 10522 additions and 3793 deletions
+4 -1
View File
@@ -5,12 +5,15 @@
"context": ".."
},
"appPort": "8124:8123",
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
"postCreateCommand": "./.devcontainer/post_create.sh",
"postStartCommand": "script/bootstrap",
"containerEnv": {
"DEV_CONTAINER": "1",
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"remoteEnv": {
"NODE_OPTIONS": "--max_old_space_size=8192"
},
"customizations": {
"vscode": {
"extensions": [
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# This script will run after the container is created
# add github cli
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
# Update package lists
sudo apt-get update
sudo apt upgrade -y
# Install necessary packages
sudo apt-get install -y libpcap-dev gh
# Display a message
echo "Post-create script has been executed successfully."
+3 -3
View File
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
path: |
node_modules/.cache/prettier
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
name: supervisor-bundle-stats
path: build/stats/*.json
+2 -2
View File
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
name: translations
path: translations.tar.gz
+42
View File
@@ -1,6 +1,42 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Develop and serve Frontend",
"type": "shell",
"command": "script/develop_and_serve -c ${input:coreUrl}",
// Sync changes here to other tasks until issue resolved
// https://github.com/Microsoft/vscode/issues/61497
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Develop Frontend",
"type": "gulp",
@@ -241,6 +277,12 @@
"id": "supervisorToken",
"type": "promptString",
"description": "The token for the Remote API proxy add-on"
},
{
"id": "coreUrl",
"type": "promptString",
"description": "The URL of the Home Assistant Core instance",
"default": "http://127.0.0.1:8123"
}
]
}
+13 -13
View File
@@ -1,16 +1,16 @@
// @ts-check
import tseslint from "typescript-eslint";
import rootConfig from "../eslint.config.mjs";
export default [
...rootConfig,
{
rules: {
"no-console": "off",
"import/no-extraneous-dependencies": "off",
"import/extensions": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
"@typescript-eslint/no-require-imports": "off",
"prefer-arrow-callback": "off",
},
export default tseslint.config(...rootConfig, {
rules: {
"no-console": "off",
"import/no-extraneous-dependencies": "off",
"import/extensions": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
"@typescript-eslint/no-require-imports": "off",
"prefer-arrow-callback": "off",
},
];
});
+8
View File
@@ -90,6 +90,14 @@ function copyMapPanel(staticDir) {
npmPath("leaflet/dist/leaflet.css"),
staticPath("images/leaflet/")
);
copyFileDir(
npmPath("leaflet.markercluster/dist/MarkerCluster.css"),
staticPath("images/leaflet/")
);
copyFileDir(
npmPath("leaflet.markercluster/dist/MarkerCluster.Default.css"),
staticPath("images/leaflet/")
);
fs.copySync(
npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/")
+2
View File
@@ -65,6 +65,7 @@ export class HaDemo extends HomeAssistantAppEl {
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",
config_subentry_id: null,
device_id: "co2signal",
area_id: null,
disabled_by: null,
@@ -85,6 +86,7 @@ export class HaDemo extends HomeAssistantAppEl {
},
{
config_entry_id: "co2signal",
config_subentry_id: null,
device_id: "co2signal",
area_id: null,
disabled_by: null,
+1
View File
@@ -11,6 +11,7 @@ export const mockConfigEntries = (hass: MockHomeAssistant) => {
supports_remove_device: false,
supports_unload: true,
supports_reconfigure: true,
supported_subentry_types: {},
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
+17 -15
View File
@@ -1,11 +1,16 @@
// @ts-check
/* eslint-disable import/no-extraneous-dependencies */
import unusedImports from "eslint-plugin-unused-imports";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";
import { configs as litConfigs } from "eslint-plugin-lit";
import { configs as wcConfigs } from "eslint-plugin-wc";
const _filename = fileURLToPath(import.meta.url);
const _dirname = path.dirname(_filename);
@@ -15,17 +20,14 @@ const compat = new FlatCompat({
allConfig: js.configs.all,
});
export default [
...compat.extends(
"airbnb-base",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/strict",
"plugin:@typescript-eslint/stylistic",
"plugin:wc/recommended",
"plugin:lit/all",
"plugin:lit-a11y/recommended",
"prettier"
),
export default tseslint.config(
...compat.extends("airbnb-base", "plugin:lit-a11y/recommended"),
eslintConfigPrettier,
litConfigs["flat/all"],
tseslint.configs.recommended,
tseslint.configs.strict,
tseslint.configs.stylistic,
wcConfigs["flat/recommended"],
{
plugins: {
"unused-imports": unusedImports,
@@ -43,7 +45,7 @@ export default [
Polymer: true,
},
parser: tsParser,
parser: tseslint.parser,
ecmaVersion: 2020,
sourceType: "module",
@@ -184,5 +186,5 @@ export default [
],
"no-use-before-define": "off",
},
},
];
}
);
+7 -7
View File
@@ -1,10 +1,10 @@
// @ts-check
import tseslint from "typescript-eslint";
import rootConfig from "../eslint.config.mjs";
export default [
...rootConfig,
{
rules: {
"no-console": "off",
},
export default tseslint.config(...rootConfig, {
rules: {
"no-console": "off",
},
];
});
+3
View File
@@ -48,6 +48,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "bedroom",
configuration_url: null,
config_entries: ["config_entry_1"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -71,6 +72,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "backyard",
configuration_url: null,
config_entries: ["config_entry_2"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -94,6 +96,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: null,
configuration_url: null,
config_entries: ["config_entry_3"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -47,6 +47,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "bedroom",
configuration_url: null,
config_entries: ["config_entry_1"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -70,6 +71,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "backyard",
configuration_url: null,
config_entries: ["config_entry_2"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -93,6 +95,7 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: null,
configuration_url: null,
config_entries: ["config_entry_3"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -32,6 +32,8 @@ const createConfigEntry = (
supports_remove_device: false,
supports_unload: true,
supports_reconfigure: true,
supported_subentry_types: {},
num_subentries: 0,
disabled_by: null,
pref_disable_new_entities: false,
pref_disable_polling: false,
@@ -188,6 +190,7 @@ const createEntityRegistryEntries = (
): EntityRegistryEntry[] => [
{
config_entry_id: item.entry_id,
config_subentry_id: null,
device_id: "mock-device-id",
area_id: null,
disabled_by: null,
@@ -214,6 +217,7 @@ const createDeviceRegistryEntries = (
{
entry_type: null,
config_entries: [item.entry_id],
config_entries_subentries: {},
connections: [],
manufacturer: "ESPHome",
model: "Mock Device",
@@ -1,8 +1,6 @@
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import { stripDiacritics } from "../../../src/common/string/strip-diacritics";
import type { StoreAddon } from "../../../src/data/supervisor/store";
import { getStripDiacriticsFn } from "../../../src/util/fuse";
export function filterAndSort(addons: StoreAddon[], filter: string) {
const options: IFuseOptions<StoreAddon> = {
@@ -10,8 +8,8 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
getFn: getStripDiacriticsFn,
ignoreDiacritics: true,
};
const fuse = new Fuse(addons, options);
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
return fuse.search(filter).map((result) => result.item);
}
@@ -14,7 +14,7 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
declare global {
interface HASSDomEvents {
"backup-uploaded": { backup: HassioBackup };
"hassio-backup-uploaded": { backup: HassioBackup };
"backup-cleared": undefined;
}
}
@@ -70,7 +70,7 @@ export class HassioUploadBackup extends LitElement {
this._uploading = true;
try {
const backup = await uploadBackup(this.hass, file);
fireEvent(this, "backup-uploaded", { backup: backup.data });
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
} catch (err: any) {
showAlertDialog(this, {
title: "Upload failed",
@@ -5,7 +5,6 @@ import { customElement, property, query } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
import type { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
@@ -19,13 +18,10 @@ import type {
} from "../../../src/data/hassio/backup";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
import type { HomeAssistant, TranslationDict } from "../../../src/types";
import type { HomeAssistant } from "../../../src/types";
import "./supervisor-formfield-label";
import type { HaTextField } from "../../../src/components/ha-textfield";
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
interface CheckboxItem {
slug: string;
checked: boolean;
@@ -67,8 +63,6 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
export class SupervisorBackupContent extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize?: LocalizeFunc;
@property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public backup?: HassioBackupDetail;
@@ -115,10 +109,6 @@ export class SupervisorBackupContent extends LitElement {
this._focusTarget?.focus();
}
private _localize = (key: BackupOrRestoreKey) =>
this.supervisor?.localize(`backup.${key}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${key}`);
protected render() {
if (!this.onboarding && !this.supervisor) {
return nothing;
@@ -132,8 +122,8 @@ export class SupervisorBackupContent extends LitElement {
${this.backup
? html`<div class="details">
${this.backup.type === "full"
? this._localize("full_backup")
: this._localize("partial_backup")}
? this.supervisor?.localize("backup.full_backup")
: this.supervisor?.localize("backup.partial_backup")}
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
${this.hass
? formatDateTime(
@@ -145,7 +135,7 @@ export class SupervisorBackupContent extends LitElement {
</div>`
: html`<ha-textfield
name="backupName"
.label=${this._localize("name")}
.label=${this.supervisor?.localize("backup.name")}
.value=${this.backupName}
@change=${this._handleTextValueChanged}
>
@@ -153,11 +143,13 @@ export class SupervisorBackupContent extends LitElement {
${!this.backup || this.backup.type === "full"
? html`<div class="sub-header">
${!this.backup
? this._localize("type")
: this._localize("select_type")}
? this.supervisor?.localize("backup.type")
: this.supervisor?.localize("backup.select_type")}
</div>
<div class="backup-types">
<ha-formfield .label=${this._localize("full_backup")}>
<ha-formfield
.label=${this.supervisor?.localize("backup.full_backup")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
value="full"
@@ -166,7 +158,9 @@ export class SupervisorBackupContent extends LitElement {
>
</ha-radio>
</ha-formfield>
<ha-formfield .label=${this._localize("partial_backup")}>
<ha-formfield
.label=${this.supervisor?.localize("backup.partial_backup")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
value="partial"
@@ -202,7 +196,7 @@ export class SupervisorBackupContent extends LitElement {
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this._localize("folders")}
.label=${this.supervisor?.localize("backup.folders")}
.iconPath=${mdiFolder}
>
</supervisor-formfield-label>`}
@@ -222,7 +216,7 @@ export class SupervisorBackupContent extends LitElement {
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this._localize("addons")}
.label=${this.supervisor?.localize("backup.addons")}
.iconPath=${mdiPuzzle}
>
</supervisor-formfield-label>`}
@@ -247,7 +241,7 @@ export class SupervisorBackupContent extends LitElement {
${!this.backup
? html`<ha-formfield
class="password"
.label=${this._localize("password_protection")}
.label=${this.supervisor?.localize("backup.password_protection")}
>
<ha-checkbox
.checked=${this.backupHasPassword}
@@ -259,7 +253,7 @@ export class SupervisorBackupContent extends LitElement {
${this.backupHasPassword
? html`
<ha-password-field
.label=${this._localize("password")}
.label=${this.supervisor?.localize("backup.password")}
name="backupPassword"
.value=${this.backupPassword}
@change=${this._handleTextValueChanged}
@@ -267,7 +261,7 @@ export class SupervisorBackupContent extends LitElement {
</ha-password-field>
${!this.backup
? html`<ha-password-field
.label=${this._localize("confirm_password")}
.label=${this.supervisor?.localize("backup.confirm_password")}
name="confirmBackupPassword"
.value=${this.confirmBackupPassword}
@change=${this._handleTextValueChanged}
@@ -72,7 +72,7 @@ export class DialogHassioBackupUpload
</ha-header-bar>
</div>
<hassio-upload-backup
@backup-uploaded=${this._backupUploaded}
@hassio-backup-uploaded=${this._backupUploaded}
.hass=${this.hass}
></hassio-upload-backup>
</ha-dialog>
@@ -35,7 +35,6 @@ import { fileDownload } from "../../../../src/util/file_download";
import "../../components/supervisor-backup-content";
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
import type { BackupOrRestoreKey } from "../../util/translations";
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
@customElement("dialog-hassio-backup")
@@ -43,7 +42,7 @@ class HassioBackupDialog
extends LitElement
implements HassDialog<HassioBackupDialogParams>
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string;
@@ -62,9 +61,13 @@ class HassioBackupDialog
this._dialogParams = dialogParams;
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
if (!this._backup) {
this._error = this._localize("no_backup_found");
this._error = this._dialogParams.supervisor?.localize(
"backup.no_backup_found"
);
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
this._error = this._localize("restore_no_home_assistant");
this._error = this._dialogParams.supervisor?.localize(
"backup.restore_no_home_assistant"
);
}
this._restoringBackup = false;
}
@@ -82,13 +85,6 @@ class HassioBackupDialog
return true;
}
private _localize(key: BackupOrRestoreKey) {
return (
this._dialogParams!.supervisor?.localize(`backup.${key}`) ||
this._dialogParams!.localize!(`ui.panel.page-onboarding.restore.${key}`)
);
}
protected render() {
if (!this._dialogParams || !this._backup) {
return nothing;
@@ -102,7 +98,7 @@ class HassioBackupDialog
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this._localize("close")}
.label=${this._dialogParams.supervisor?.localize("backup.close")}
.path=${mdiClose}
@click=${this.closeDialog}
.disabled=${this._restoringBackup}
@@ -150,7 +146,6 @@ class HassioBackupDialog
.supervisor=${this._dialogParams.supervisor}
.backup=${this._backup}
.onboarding=${this._dialogParams.onboarding || false}
.localize=${this._dialogParams.localize}
dialogInitialFocus
>
</supervisor-backup-content>
@@ -161,7 +156,7 @@ class HassioBackupDialog
.disabled=${this._restoringBackup || !!this._error}
@click=${this._restoreClicked}
>
${this._localize("restore")}
${this._dialogParams.supervisor?.localize("backup.restore")}
</ha-button>
</div>
</ha-md-dialog>
@@ -196,18 +191,22 @@ class HassioBackupDialog
}
if (
!(await showConfirmationDialog(this, {
title: this._localize(
this._backup!.type === "full"
? "confirm_restore_full_backup_title"
: "confirm_restore_partial_backup_title"
title: supervisor?.localize(
`backup.${
this._backup!.type === "full"
? "confirm_restore_full_backup_title"
: "confirm_restore_partial_backup_title"
}`
),
text: this._localize(
this._backup!.type === "full"
? "confirm_restore_full_backup_text"
: "confirm_restore_partial_backup_text"
text: supervisor?.localize(
`backup.${
this._backup!.type === "full"
? "confirm_restore_full_backup_text"
: "confirm_restore_partial_backup_text"
}`
),
confirmText: this._localize("restore"),
dismissText: this._localize("cancel"),
confirmText: supervisor?.localize("backup.restore"),
dismissText: supervisor?.localize("backup.cancel"),
}))
) {
this._restoringBackup = false;
@@ -227,7 +226,8 @@ class HassioBackupDialog
this.closeDialog();
} catch (error: any) {
this._error =
error?.body?.message || this._localize("restore_start_failed");
error?.body?.message ||
supervisor?.localize("backup.restore_start_failed");
} finally {
this._restoringBackup = false;
}
@@ -286,7 +286,7 @@ class HassioBackupDialog
title: supervisor.localize("backup.remote_download_title"),
text: supervisor.localize("backup.remote_download_text"),
confirmText: supervisor.localize("backup.download"),
dismissText: this._localize("cancel"),
dismissText: supervisor?.localize("backup.cancel"),
});
if (!confirm) {
return;
@@ -302,7 +302,7 @@ class HassioBackupDialog
private get _computeName() {
return this._backup
? this._backup.name || this._backup.slug
: this._localize("unnamed_backup");
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
}
static get styles(): CSSResultGroup {
@@ -1,5 +1,4 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { LocalizeFunc } from "../../../../src/common/translations/localize";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupDialogParams {
@@ -8,7 +7,6 @@ export interface HassioBackupDialogParams {
onRestoring?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
localize?: LocalizeFunc;
}
export const showHassioBackupDialog = (
-4
View File
@@ -1,4 +0,0 @@
import type { TranslationDict } from "../../../src/types";
export type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
@@ -17,6 +17,7 @@ import "../../../src/components/ha-alert";
import {
ALTERNATIVE_DNS_SERVERS,
getSupervisorNetworkInfo,
pingSupervisor,
setSupervisorNetworkDns,
} from "../data/supervisor";
import { fireEvent } from "../../../src/common/dom/fire_event";
@@ -85,7 +86,28 @@ class LandingPageNetwork extends LitElement {
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
this._fetchSupervisorInfo();
this._pingSupervisor();
}
private _schedulePingSupervisor() {
setTimeout(
() => this._pingSupervisor(),
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
);
}
private async _pingSupervisor() {
try {
const response = await pingSupervisor();
if (!response.ok) {
throw new Error("Failed to ping supervisor, assume update in progress");
}
this._fetchSupervisorInfo();
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
this._schedulePingSupervisor();
}
}
private _scheduleFetchSupervisorInfo() {
+8 -4
View File
@@ -18,7 +18,7 @@ export const ALTERNATIVE_DNS_SERVERS: {
];
export async function getSupervisorLogs(lines = 100) {
return fetch(`/supervisor/supervisor/logs?lines=${lines}`, {
return fetch(`/supervisor-api/supervisor/logs?lines=${lines}`, {
headers: {
Accept: "text/plain",
},
@@ -26,22 +26,26 @@ export async function getSupervisorLogs(lines = 100) {
}
export async function getSupervisorLogsFollow(lines = 500) {
return fetch(`/supervisor/supervisor/logs/follow?lines=${lines}`, {
return fetch(`/supervisor-api/supervisor/logs/follow?lines=${lines}`, {
headers: {
Accept: "text/plain",
},
});
}
export async function pingSupervisor() {
return fetch("/supervisor-api/supervisor/ping");
}
export async function getSupervisorNetworkInfo() {
return fetch("/supervisor/network/info");
return fetch("/supervisor-api/network/info");
}
export const setSupervisorNetworkDns = async (
dnsServerIndex: number,
primaryInterface: string
) =>
fetch(`/supervisor/network/interface/${primaryInterface}/update`, {
fetch(`/supervisor-api/network/interface/${primaryInterface}/update`, {
method: "POST",
body: JSON.stringify({
ipv4: {
+47 -43
View File
@@ -26,25 +26,25 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.26.7",
"@babel/runtime": "7.26.9",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.4",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/commands": "6.8.0",
"@codemirror/language": "6.10.8",
"@codemirror/legacy-modes": "6.4.2",
"@codemirror/search": "6.5.8",
"@codemirror/state": "6.5.1",
"@codemirror/view": "6.36.2",
"@codemirror/legacy-modes": "6.4.3",
"@codemirror/search": "6.5.9",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.3",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.17.2",
"@formatjs/intl-displaynames": "6.8.9",
"@formatjs/intl-durationformat": "0.7.2",
"@formatjs/intl-datetimeformat": "6.17.3",
"@formatjs/intl-displaynames": "6.8.10",
"@formatjs/intl-durationformat": "0.7.3",
"@formatjs/intl-getcanonicallocales": "2.5.4",
"@formatjs/intl-listformat": "7.7.9",
"@formatjs/intl-locale": "4.2.9",
"@formatjs/intl-numberformat": "8.15.2",
"@formatjs/intl-pluralrules": "5.4.2",
"@formatjs/intl-relativetimeformat": "11.4.9",
"@formatjs/intl-listformat": "7.7.10",
"@formatjs/intl-locale": "4.2.10",
"@formatjs/intl-numberformat": "8.15.3",
"@formatjs/intl-pluralrules": "5.4.3",
"@formatjs/intl-relativetimeformat": "11.4.10",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -53,9 +53,9 @@
"@fullcalendar/timegrid": "6.1.15",
"@lezer/highlight": "1.2.1",
"@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.7",
"@lit-labs/observers": "2.0.4",
"@lit-labs/virtualizer": "2.0.15",
"@lit-labs/motion": "1.0.8",
"@lit-labs/observers": "2.0.5",
"@lit-labs/virtualizer": "2.1.0",
"@lrnwebcomponents/simple-tooltip": "8.0.2",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
@@ -90,15 +90,16 @@
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@shoelace-style/shoelace": "2.20.0",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.6.2",
"@vaadin/vaadin-themable-mixin": "24.6.2",
"@vaadin/combo-box": "24.6.5",
"@vaadin/vaadin-themable-mixin": "24.6.5",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "2.3.1",
"barcode-detector": "3.0.0",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.40.0",
@@ -110,22 +111,24 @@
"dialog-polyfill": "0.5.6",
"echarts": "5.6.0",
"element-internals-polyfill": "1.3.13",
"fuse.js": "7.0.0",
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.7.14",
"intl-messageformat": "10.7.15",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"leaflet.markercluster": "1.5.3",
"lit": "2.8.0",
"lit-html": "2.8.0",
"luxon": "3.5.0",
"marked": "15.0.6",
"marked": "15.0.7",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
"punycode": "2.3.1",
"qr-scanner": "1.4.2",
"qrcode": "1.5.4",
@@ -137,7 +140,7 @@
"tinykeys": "3.0.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "2.0.0",
"ua-parser-js": "2.0.2",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
"vue": "2.7.16",
@@ -152,22 +155,22 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.26.7",
"@babel/core": "7.26.9",
"@babel/helper-define-polyfill-provider": "0.6.3",
"@babel/plugin-proposal-decorators": "7.25.9",
"@babel/plugin-transform-runtime": "7.25.9",
"@babel/preset-env": "7.26.7",
"@babel/plugin-transform-runtime": "7.26.9",
"@babel/preset-env": "7.26.9",
"@babel/preset-typescript": "7.26.0",
"@bundle-stats/plugin-webpack-filter": "4.18.2",
"@lokalise/node-api": "13.0.0",
"@octokit/auth-oauth-device": "7.1.2",
"@octokit/plugin-retry": "7.1.3",
"@octokit/rest": "21.1.0",
"@lokalise/node-api": "13.2.0",
"@octokit/auth-oauth-device": "7.1.3",
"@octokit/plugin-retry": "7.1.4",
"@octokit/rest": "21.1.1",
"@rsdoctor/rspack-plugin": "0.4.13",
"@rspack/cli": "1.2.2",
"@rspack/core": "1.2.2",
"@rspack/cli": "1.2.5",
"@rspack/core": "1.2.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.20",
"@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-sender": "1.0.11",
"@types/color-name": "2.0.0",
"@types/glob": "8.1.0",
@@ -175,6 +178,7 @@
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.16",
"@types/leaflet-draw": "1.0.11",
"@types/leaflet.markercluster": "1.5.5",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2",
"@types/mocha": "10.0.10",
@@ -183,14 +187,12 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "8.21.0",
"@typescript-eslint/parser": "8.21.0",
"@vitest/coverage-v8": "3.0.4",
"@vitest/coverage-v8": "3.0.6",
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.19.0",
"eslint": "9.20.1",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.0.1",
"eslint-import-resolver-webpack": "0.13.10",
@@ -198,7 +200,7 @@
"eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "2.2.0",
"eslint-plugin-wc": "2.2.1",
"fancy-log": "2.0.0",
"fs-extra": "11.3.0",
"glob": "11.0.1",
@@ -215,16 +217,18 @@
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"map-stream": "0.0.7",
"object-hash": "3.0.0",
"pinst": "3.0.0",
"prettier": "3.4.2",
"prettier": "3.5.1",
"rspack-manifest-plugin": "5.0.3",
"serve": "14.2.4",
"sinon": "19.0.2",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.11",
"ts-lit-plugin": "2.0.2",
"typescript": "5.7.3",
"vitest": "3.0.4",
"typescript-eslint": "8.24.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.0.6",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -238,7 +242,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15",
"globals": "15.14.0",
"globals": "16.0.0",
"tslib": "2.8.1"
},
"packageManager": "[email protected]"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20250129.0"
version = "20250205.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
+1 -1
View File
@@ -64,7 +64,7 @@ echo Core is used from ${coreUrl}
HASS_URL="$coreUrl" ./script/develop &
# serve the frontend
yarn dlx serve -l $frontendPort ./hass_frontend -s &
./node_modules/.bin/serve -p $frontendPort --single --no-port-switching --config ../script/serve-config.json ./hass_frontend &
# keep the script running while serving
wait
+3
View File
@@ -0,0 +1,3 @@
{
"cleanUrls": false
}
+10
View File
@@ -1,3 +1,4 @@
import memoizeOne from "memoize-one";
import { theme2hex } from "./convert-color";
export const COLORS = [
@@ -74,3 +75,12 @@ export function getGraphColorByIndex(
getColorByIndex(index);
return theme2hex(themeColor);
}
export const getAllGraphColors = memoizeOne(
(style: CSSStyleDeclaration) =>
COLORS.map((_color, index) => getGraphColorByIndex(index, style)),
(newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) =>
// this is not ideal, but we need to memoize the colors
newArgs[0].getPropertyValue("--graph-color-1") ===
lastArgs[0].getPropertyValue("--graph-color-1")
);
+13 -6
View File
@@ -136,11 +136,18 @@ export function theme2hex(themeColor: string): string {
}
const rgbFromColorName = colors[themeColor];
if (!rgbFromColorName) {
// We have a named color, and there's nothing in the table,
// so nothing further we can do with it.
// Compare/border/background color will all be the same.
return themeColor;
if (rgbFromColorName) {
return rgb2hex(rgbFromColorName);
}
return rgb2hex(rgbFromColorName);
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (rgbMatch) {
const [, r, g, b] = rgbMatch.map(Number);
return rgb2hex([r, g, b]);
}
// We have a named color, and there's nothing in the table,
// so nothing further we can do with it.
// Compare/border/background color will all be the same.
return themeColor;
}
+14
View File
@@ -26,6 +26,20 @@ const formatDateTimeMem = memoizeOne(
})
);
export const formatDateTimeWithBrowserDefaults = (dateObj: Date) =>
formatDateTimeWithBrowserDefaultsMem().format(dateObj);
const formatDateTimeWithBrowserDefaultsMem = memoizeOne(
() =>
new Intl.DateTimeFormat(undefined, {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})
);
// Aug 9, 2021, 8:23 AM
export const formatShortDateTimeWithYear = (
dateObj: Date,
+19
View File
@@ -16,11 +16,30 @@ export const setupLeafletMap = async (
const Leaflet = (await import("leaflet")).default as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
await import("leaflet.markercluster");
const map = Leaflet.map(mapElement);
const style = document.createElement("link");
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
style.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(style);
const markerClusterStyle = document.createElement("link");
markerClusterStyle.setAttribute(
"href",
"/static/images/leaflet/MarkerCluster.css"
);
markerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(markerClusterStyle);
const defaultMarkerClusterStyle = document.createElement("link");
defaultMarkerClusterStyle.setAttribute(
"href",
"/static/images/leaflet/MarkerCluster.Default.css"
);
defaultMarkerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(defaultMarkerClusterStyle);
map.setView([52.3731339, 4.8903147], 13);
const tileLayer = createTileLayer(Leaflet).addTo(map);
+1 -1
View File
@@ -1,2 +1,2 @@
export const computeDomain = (entityId: string): string =>
entityId.substr(0, entityId.indexOf("."));
entityId.substring(0, entityId.indexOf("."));
+1 -5
View File
@@ -120,11 +120,6 @@ export const computeStateDisplayFromEntityAttributes = (
return value;
}
if (domain === "datetime") {
const time = new Date(state);
return formatDateTime(time, locale, config);
}
if (["date", "input_datetime", "time"].includes(domain)) {
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
// Attributes aren't available, we have to use `state`.
@@ -181,6 +176,7 @@ export const computeStateDisplayFromEntityAttributes = (
"tag",
"tts",
"wake_word",
"datetime",
].includes(domain) ||
(domain === "sensor" && attributes.device_class === "timestamp")
) {
+17 -1
View File
@@ -1,6 +1,9 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
import { UNAVAILABLE_STATES } from "../../data/entity";
import type { HomeAssistant } from "../../types";
import { computeDomain } from "./compute_domain";
import { stringCompare } from "../string/compare";
export const FIXED_DOMAIN_STATES = {
alarm_control_panel: [
@@ -237,6 +240,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
};
export const getStates = (
hass: HomeAssistant,
state: HassEntity,
attribute: string | undefined = undefined
): string[] => {
@@ -269,7 +273,19 @@ export const getStates = (
case "device_tracker":
case "person":
if (!attribute) {
result.push("home", "not_home");
result.push(
...Object.entries(hass.states)
.filter(
([entityId, stateObj]) =>
computeDomain(entityId) === "zone" &&
entityId !== "zone.home" &&
stateObj.attributes.friendly_name
)
.map(([_entityId, stateObj]) => stateObj.attributes.friendly_name!)
.sort((zone1, zone2) =>
stringCompare(zone1, zone2, hass.locale.language)
)
);
}
break;
case "event":
+32
View File
@@ -0,0 +1,32 @@
import type { LatLngExpression, Layer, Map, MarkerOptions } from "leaflet";
import { Marker } from "leaflet";
export class DecoratedMarker extends Marker {
decorationLayer: Layer | undefined;
constructor(
latlng: LatLngExpression,
decorationLayer?: Layer,
options?: MarkerOptions
) {
super(latlng, options);
this.decorationLayer = decorationLayer;
}
onAdd(map: Map) {
super.onAdd(map);
// If decoration has been provided, add it to the map as well
this.decorationLayer?.addTo(map);
return this;
}
onRemove(map: Map) {
// If decoration has been provided, remove it from the map as well
this.decorationLayer?.remove();
return super.onRemove(map);
}
}
+4 -1
View File
@@ -41,7 +41,7 @@ export class HaProgressButton extends LitElement {
indeterminate
></ha-circular-progress>
`
: ""}
: nothing}
</div>
`}
`;
@@ -117,6 +117,9 @@ export class HaProgressButton extends LitElement {
mwc-button.error slot {
visibility: hidden;
}
:host([destructive]) {
--mdc-theme-primary: var(--error-color);
}
`;
}
+37 -48
View File
@@ -1,5 +1,4 @@
import type { HassConfig } from "home-assistant-js-websocket";
import type { XAXisOption } from "echarts/types/dist/shared";
import type { FrontendLocaleData } from "../../data/translation";
import {
formatDateMonth,
@@ -7,56 +6,46 @@ import {
formatDateVeryShort,
formatDateWeekdayShort,
} from "../../common/datetime/format_date";
import { formatTime } from "../../common/datetime/format_time";
import {
formatTime,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
export function getLabelFormatter(
export function formatTimeLabel(
value: number | Date,
locale: FrontendLocaleData,
config: HassConfig,
dayDifference = 0
minutesDifference: number
) {
return (value: number | Date) => {
const date = new Date(value);
if (dayDifference > 88) {
return date.getMonth() === 0
? `{bold|${formatDateMonthYear(date, locale, config)}}`
: formatDateMonth(date, locale, config);
}
if (dayDifference > 35) {
return date.getDate() === 1
? `{bold|${formatDateVeryShort(date, locale, config)}}`
: formatDateVeryShort(date, locale, config);
}
if (dayDifference > 7) {
const label = formatDateVeryShort(date, locale, config);
return date.getDate() === 1 ? `{bold|${label}}` : label;
}
if (dayDifference > 2) {
return formatDateWeekdayShort(date, locale, config);
}
const dayDifference = minutesDifference / 60 / 24;
const date = new Date(value);
if (dayDifference > 88) {
return date.getMonth() === 0
? `{bold|${formatDateMonthYear(date, locale, config)}}`
: formatDateMonth(date, locale, config);
}
if (dayDifference > 35) {
return date.getDate() === 1
? `{bold|${formatDateVeryShort(date, locale, config)}}`
: formatDateVeryShort(date, locale, config);
}
if (dayDifference > 7) {
const label = formatDateVeryShort(date, locale, config);
return date.getDate() === 1 ? `{bold|${label}}` : label;
}
if (dayDifference > 2) {
return formatDateWeekdayShort(date, locale, config);
}
if (minutesDifference && minutesDifference < 5) {
return formatTimeWithSeconds(date, locale, config);
}
if (
date.getHours() === 0 &&
date.getMinutes() === 0 &&
date.getSeconds() === 0
) {
// show only date for the beginning of the day
if (
date.getHours() === 0 &&
date.getMinutes() === 0 &&
date.getSeconds() === 0
) {
return `{bold|${formatDateVeryShort(date, locale, config)}}`;
}
return formatTime(date, locale, config);
};
}
export function getTimeAxisLabelConfig(
locale: FrontendLocaleData,
config: HassConfig,
dayDifference?: number
): XAXisOption["axisLabel"] {
return {
formatter: getLabelFormatter(locale, config, dayDifference),
rich: {
bold: {
fontWeight: "bold",
},
},
hideOverlap: true,
};
return `{bold|${formatDateVeryShort(date, locale, config)}}`;
}
return formatTime(date, locale, config);
}
+326 -80
View File
@@ -1,25 +1,30 @@
import type { PropertyValues } from "lit";
import { css, html, nothing, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { mdiRestart } from "@mdi/js";
import type { EChartsType } from "echarts/core";
import type { DataZoomComponentOption } from "echarts/components";
import { consume } from "@lit-labs/context";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiRestart } from "@mdi/js";
import { differenceInMinutes } from "date-fns";
import type { DataZoomComponentOption } from "echarts/components";
import type { EChartsType } from "echarts/core";
import type {
ECElementEvent,
XAXisOption,
YAXisOption,
} from "echarts/types/dist/shared";
import { consume } from "@lit-labs/context";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { getAllGraphColors } from "../../common/color/colors";
import { fireEvent } from "../../common/dom/fire_event";
import { listenMediaQuery } from "../../common/dom/media_query";
import { themesContext } from "../../data/context";
import type { Themes } from "../../data/ws-themes";
import type { ECOption } from "../../resources/echarts";
import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
import "../ha-icon-button";
import type { ECOption } from "../../resources/echarts";
import { listenMediaQuery } from "../../common/dom/media_query";
import type { Themes } from "../../data/ws-themes";
import { themesContext } from "../../data/context";
import { formatTimeLabel } from "./axis-label";
import { ensureArray } from "../../common/array/ensure-array";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
@@ -44,6 +49,10 @@ export class HaChartBase extends LitElement {
@state() private _isZoomed = false;
@state() private _zoomRatio = 1;
@state() private _minutesDifference = 24 * 60;
private _modifierPressed = false;
private _isTouchDevice = "ontouchstart" in window;
@@ -59,12 +68,16 @@ export class HaChartBase extends LitElement {
private _listeners: (() => void)[] = [];
private _originalZrFlush?: () => void;
public disconnectedCallback() {
super.disconnectedCallback();
while (this._listeners.length) {
this._listeners.pop()!();
}
this.chart?.dispose();
this.chart = undefined;
this._originalZrFlush = undefined;
}
public connectedCallback() {
@@ -75,19 +88,19 @@ export class HaChartBase extends LitElement {
this._listeners.push(
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
this._reducedMotion = matches;
this.chart?.setOption({ animation: !this._reducedMotion });
if (this._reducedMotion !== matches) {
this._reducedMotion = matches;
this._setChartOptions({ animation: !this._reducedMotion });
}
})
);
// Add keyboard event listeners
const handleKeyDown = (ev: KeyboardEvent) => {
if ((isMac && ev.metaKey) || (!isMac && ev.ctrlKey)) {
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
this._modifierPressed = true;
if (!this.options?.dataZoom) {
this.chart?.setOption({
dataZoom: this._getDataZoomConfig(),
});
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
}
}
};
@@ -96,9 +109,7 @@ export class HaChartBase extends LitElement {
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
this._modifierPressed = false;
if (!this.options?.dataZoom) {
this.chart?.setOption({
dataZoom: this._getDataZoomConfig(),
});
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
}
}
};
@@ -116,47 +127,37 @@ export class HaChartBase extends LitElement {
}
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this.hasUpdated || !this.chart) {
if (!this.chart) {
return;
}
if (changedProps.has("_themes")) {
this._setupChart();
return;
}
let chartOptions: ECOption = {};
if (changedProps.has("data")) {
this.chart.setOption(
{ series: this.data },
{ lazyUpdate: true, replaceMerge: ["series"] }
);
chartOptions.series = this.data;
}
if (changedProps.has("options") || changedProps.has("_isZoomed")) {
this.chart.setOption(this._createOptions(), {
lazyUpdate: true,
// if we replace the whole object, it will reset the dataZoom
replaceMerge: [
"xAxis",
"yAxis",
"dataZoom",
"dataset",
"tooltip",
"legend",
"grid",
"visualMap",
],
});
if (changedProps.has("options")) {
chartOptions = { ...chartOptions, ...this._createOptions() };
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
chartOptions.dataZoom = this._getDataZoomConfig();
}
if (Object.keys(chartOptions).length > 0) {
this._setChartOptions(chartOptions);
}
}
protected render() {
return html`
<div
class="chart-container"
class=${classMap({
"chart-container": true,
"has-legend": !!this.options?.legend,
})}
style=${styleMap({
height: this.height ?? `${this._getDefaultHeight()}px`,
})}
@wheel=${this._handleWheel}
>
<div class="chart"></div>
${this._isZoomed
@@ -173,6 +174,14 @@ export class HaChartBase extends LitElement {
`;
}
private _formatTimeLabel = (value: number | Date) =>
formatTimeLabel(
value,
this.hass.locale,
this.hass.config,
this._minutesDifference * this._zoomRatio
);
private async _setupChart() {
if (this._loading) return;
const container = this.renderRoot.querySelector(".chart") as HTMLDivElement;
@@ -183,10 +192,9 @@ export class HaChartBase extends LitElement {
}
const echarts = (await import("../../resources/echarts")).default;
this.chart = echarts.init(
container,
this._themes.darkMode ? "dark" : "light"
);
echarts.registerTheme("custom", this._createTheme());
this.chart = echarts.init(container, "custom");
this.chart.on("legendselectchanged", (params: any) => {
if (this.externalHidden) {
const isSelected = params.selected[params.name];
@@ -200,6 +208,7 @@ export class HaChartBase extends LitElement {
this.chart.on("datazoom", (e: any) => {
const { start, end } = e.batch?.[0] ?? e;
this._isZoomed = start !== 0 || end !== 100;
this._zoomRatio = (end - start) / 100;
});
this.chart.on("click", (e: ECElementEvent) => {
fireEvent(this, "chart-click", e);
@@ -230,31 +239,67 @@ export class HaChartBase extends LitElement {
type: "inside",
orient: "horizontal",
filterMode: "none",
moveOnMouseMove: this._isZoomed,
preventDefaultMouseMove: this._isZoomed,
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
zoomLock: !this._isTouchDevice && !this._modifierPressed,
};
}
private _createOptions(): ECOption {
const darkMode = this._themes.darkMode ?? false;
let xAxis = this.options?.xAxis;
if (xAxis) {
xAxis = Array.isArray(xAxis) ? xAxis : [xAxis];
xAxis = xAxis.map((axis: XAXisOption) => {
if (axis.type !== "time" || axis.show === false) {
return axis;
}
if (axis.max && axis.min) {
this._minutesDifference = differenceInMinutes(
axis.max as Date,
axis.min as Date
);
}
const dayDifference = this._minutesDifference / 60 / 24;
let minInterval: number | undefined;
if (dayDifference) {
minInterval =
dayDifference >= 89 // quarter
? 28 * 3600 * 24 * 1000
: dayDifference > 2
? 3600 * 24 * 1000
: undefined;
}
return {
axisLine: {
show: false,
},
splitLine: {
show: true,
},
...axis,
axisLabel: {
formatter: this._formatTimeLabel,
rich: {
bold: {
fontWeight: "bold",
},
},
hideOverlap: true,
...axis.axisLabel,
},
minInterval,
} as XAXisOption;
});
}
const options = {
backgroundColor: "transparent",
animation: !this._reducedMotion,
darkMode,
darkMode: this._themes.darkMode ?? false,
aria: {
show: true,
},
dataZoom: this._getDataZoomConfig(),
...this.options,
legend: this.options?.legend
? {
// we should create our own theme but this is a quick fix for now
inactiveColor: darkMode ? "#444" : "#ccc",
...this.options.legend,
}
: undefined,
xAxis,
};
const isMobile = window.matchMedia(
@@ -268,44 +313,242 @@ export class HaChartBase extends LitElement {
tooltips.forEach((tooltip) => {
tooltip.confine = true;
tooltip.appendTo = undefined;
tooltip.triggerOn = "click";
});
options.tooltip = tooltips;
}
return options;
}
private _createTheme() {
const style = getComputedStyle(this);
return {
color: getAllGraphColors(style),
backgroundColor: "transparent",
textStyle: {
color: style.getPropertyValue("--primary-text-color"),
fontFamily: "Roboto, Noto, sans-serif",
},
title: {
textStyle: {
color: style.getPropertyValue("--primary-text-color"),
},
subtextStyle: {
color: style.getPropertyValue("--secondary-text-color"),
},
},
line: {
lineStyle: {
width: 1.5,
},
symbolSize: 1,
symbol: "circle",
smooth: false,
},
bar: {
itemStyle: {
barBorderWidth: 1.5,
},
},
categoryAxis: {
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
show: true,
color: style.getPropertyValue("--primary-text-color"),
},
splitLine: {
show: false,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
},
splitArea: {
show: false,
areaStyle: {
color: [
style.getPropertyValue("--divider-color") + "3F",
style.getPropertyValue("--divider-color") + "7F",
],
},
},
},
valueAxis: {
axisLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
},
axisTick: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
},
axisLabel: {
show: true,
color: style.getPropertyValue("--primary-text-color"),
},
splitLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
},
splitArea: {
show: false,
areaStyle: {
color: [
style.getPropertyValue("--divider-color") + "3F",
style.getPropertyValue("--divider-color") + "7F",
],
},
},
},
logAxis: {
axisLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
},
axisTick: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
},
axisLabel: {
show: true,
color: style.getPropertyValue("--primary-text-color"),
},
splitLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
},
splitArea: {
show: false,
areaStyle: {
color: [
style.getPropertyValue("--divider-color") + "3F",
style.getPropertyValue("--divider-color") + "7F",
],
},
},
},
timeAxis: {
axisLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
},
axisTick: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
},
axisLabel: {
show: true,
color: style.getPropertyValue("--primary-text-color"),
},
splitLine: {
show: true,
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
},
splitArea: {
show: false,
areaStyle: {
color: [
style.getPropertyValue("--divider-color") + "3F",
style.getPropertyValue("--divider-color") + "7F",
],
},
},
},
legend: {
textStyle: {
color: style.getPropertyValue("--primary-text-color"),
},
inactiveColor: style.getPropertyValue("--disabled-text-color"),
pageIconColor: style.getPropertyValue("--primary-text-color"),
pageIconInactiveColor: style.getPropertyValue("--disabled-text-color"),
pageTextStyle: {
color: style.getPropertyValue("--secondary-text-color"),
},
},
tooltip: {
backgroundColor: style.getPropertyValue("--card-background-color"),
borderColor: style.getPropertyValue("--divider-color"),
textStyle: {
color: style.getPropertyValue("--primary-text-color"),
fontSize: 12,
},
axisPointer: {
lineStyle: {
color: style.getPropertyValue("--divider-color"),
},
crossStyle: {
color: style.getPropertyValue("--divider-color"),
},
},
},
timeline: {},
};
}
private _getDefaultHeight() {
return Math.max(this.clientWidth / 2, 400);
return Math.max(this.clientWidth / 2, 200);
}
private _setChartOptions(options: ECOption) {
if (!this.chart) {
return;
}
if (!this._originalZrFlush) {
const dataSize = ensureArray(this.data).reduce(
(acc, series) => acc + (series.data as any[]).length,
0
);
if (dataSize > 10000) {
// delay the last bit of the render to avoid blocking the main thread
// this is not that impactful with sampling enabled but it doesn't hurt to have it
const zr = this.chart.getZr();
this._originalZrFlush = zr.flush;
zr.flush = () => {
setTimeout(() => {
this._originalZrFlush?.call(zr);
}, 5);
};
}
}
const replaceMerge = options.series ? ["series"] : [];
this.chart.setOption(options, { replaceMerge });
}
private _handleZoomReset() {
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
}
private _handleWheel(e: WheelEvent) {
// if the window is not focused, we don't receive the keydown events but scroll still works
if (!this.options?.dataZoom) {
const modifierPressed = (isMac && e.metaKey) || (!isMac && e.ctrlKey);
if (modifierPressed) {
e.preventDefault();
}
if (modifierPressed !== this._modifierPressed) {
this._modifierPressed = modifierPressed;
this.chart?.setOption({
dataZoom: this._getDataZoomConfig(),
});
}
}
}
static styles = css`
:host {
display: block;
position: relative;
letter-spacing: normal;
}
.chart-container {
position: relative;
max-height: var(--chart-max-height, 400px);
max-height: var(--chart-max-height, 350px);
}
.chart {
width: 100%;
@@ -321,6 +564,9 @@ export class HaChartBase extends LitElement {
color: var(--primary-color);
border: 1px solid var(--divider-color);
}
.has-legend .zoom-reset {
top: 64px;
}
`;
}
+266 -132
View File
@@ -4,7 +4,6 @@ import { property, state } from "lit/decorators";
import type { VisualMapComponentOption } from "echarts/components";
import type { LineSeriesOption } from "echarts/charts";
import type { YAXisOption } from "echarts/types/dist/shared";
import { differenceInDays } from "date-fns";
import { styleMap } from "lit/directives/style-map";
import { getGraphColorByIndex } from "../../common/color/colors";
import { computeRTL } from "../../common/util/compute_rtl";
@@ -18,10 +17,10 @@ import {
getNumberFormatOptions,
formatNumber,
} from "../../common/number/format_number";
import { getTimeAxisLabelConfig } from "./axis-label";
import { measureTextWidth } from "../../util/text";
import { fireEvent } from "../../common/dom/fire_event";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
const safeParseFloat = (value) => {
const parsed = parseFloat(value);
@@ -72,8 +71,12 @@ export class StateHistoryChartLine extends LitElement {
@state() private _chartOptions?: ECOption;
private _hiddenStats = new Set<string>();
@state() private _yWidth = 25;
@state() private _visualMap?: VisualMapComponentOption[];
private _chartTime: Date = new Date();
protected render() {
@@ -84,49 +87,104 @@ export class StateHistoryChartLine extends LitElement {
.options=${this._chartOptions}
.height=${this.height}
style=${styleMap({ height: this.height })}
external-hidden
@dataset-hidden=${this._datasetHidden}
@dataset-unhidden=${this._datasetUnhidden}
></ha-chart-base>
`;
}
private _renderTooltip(params) {
return params
.map((param, index: number) => {
let value = `${formatNumber(
param.value[1] as number,
this.hass.locale,
getNumberFormatOptions(
undefined,
this.hass.entities[this._entityIds[param.seriesIndex]]
)
)} ${this.unit}`;
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
const data = this.data[dataIndex];
if (data.statistics && data.statistics.length > 0) {
value += "<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
const source =
data.states.length === 0 ||
param.value[0] < data.states[0].last_changed
? `${this.hass.localize(
"ui.components.history_charts.source_stats"
)}`
: `${this.hass.localize(
"ui.components.history_charts.source_history"
)}`;
value += source;
private _renderTooltip = (params: any) => {
const time = params[0].axisValue;
const title =
formatDateTimeWithSeconds(
new Date(time),
this.hass.locale,
this.hass.config
) + "<br>";
const datapoints: Record<string, any>[] = [];
this._chartData.forEach((dataset, index) => {
if (
dataset.tooltip?.show === false ||
this._hiddenStats.has(dataset.name as string)
)
return;
const param = params.find(
(p: Record<string, any>) => p.seriesIndex === index
);
if (param) {
datapoints.push(param);
return;
}
// If the datapoint is not found, we need to find the last datapoint before the current time
let lastData: any;
const data = dataset.data || [];
for (let i = data.length - 1; i >= 0; i--) {
const point = data[i];
if (point && point[0] <= time && point[1]) {
lastData = point;
break;
}
}
if (!lastData) return;
datapoints.push({
seriesName: dataset.name,
seriesIndex: index,
value: lastData,
// HTML copied from echarts. May change based on options
marker: `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${dataset.color};"></span>`,
});
});
const unit = this.unit
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
: "";
const time =
index === 0
? formatDateTimeWithSeconds(
new Date(param.value[0]),
return (
title +
datapoints
.map((param) => {
const entityId = this._entityIds[param.seriesIndex];
const stateObj = this.hass.states[entityId];
const entry = this.hass.entities[entityId];
const stateValue = String(param.value[1]);
let value = stateObj
? this.hass.formatEntityState(stateObj, stateValue)
: `${formatNumber(
stateValue,
this.hass.locale,
this.hass.config
) + "<br>"
: "";
return `${time}${param.marker} ${param.seriesName}: ${value}
`;
})
.join("<br>");
getNumberFormatOptions(undefined, entry)
)}${unit}`;
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
const data = this.data[dataIndex];
if (data.statistics && data.statistics.length > 0) {
value += "<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
const source =
data.states.length === 0 ||
param.value[0] < data.states[0].last_changed
? `${this.hass.localize(
"ui.components.history_charts.source_stats"
)}`
: `${this.hass.localize(
"ui.components.history_charts.source_history"
)}`;
value += source;
}
if (param.seriesName) {
return `${param.marker} ${param.seriesName}: ${value}`;
}
return `${param.marker} ${value}`;
})
.join("<br>")
);
};
private _datasetHidden(ev: CustomEvent) {
this._hiddenStats.add(ev.detail.name);
}
private _datasetUnhidden(ev: CustomEvent) {
this._hiddenStats.delete(ev.detail.name);
}
public willUpdate(changedProps: PropertyValues) {
@@ -152,53 +210,48 @@ export class StateHistoryChartLine extends LitElement {
changedProps.has("minYAxis") ||
changedProps.has("maxYAxis") ||
changedProps.has("fitYData") ||
changedProps.has("_chartData") ||
changedProps.has("paddingYAxis") ||
changedProps.has("_visualMap") ||
changedProps.has("_yWidth")
) {
const dayDifference = differenceInDays(this.endTime, this.startTime);
const rtl = computeRTL(this.hass);
const splitLineStyle = this.hass.themes?.darkMode
? { opacity: 0.15 }
: {};
let minYAxis: number | ((values: { min: number }) => number) | undefined =
this.minYAxis;
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
this.maxYAxis;
if (typeof minYAxis === "number") {
if (this.fitYData) {
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
}
} else if (this.logarithmicScale) {
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
}
if (typeof maxYAxis === "number") {
if (this.fitYData) {
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
}
} else if (this.logarithmicScale) {
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
}
this._chartOptions = {
xAxis: {
type: "time",
min: this.startTime,
max: this.endTime,
axisLabel: getTimeAxisLabelConfig(
this.hass.locale,
this.hass.config,
dayDifference
),
axisLine: {
show: false,
},
splitLine: {
show: true,
lineStyle: splitLineStyle,
},
minInterval:
dayDifference >= 89 // quarter
? 28 * 3600 * 24 * 1000
: dayDifference > 2
? 3600 * 24 * 1000
: undefined,
},
yAxis: {
type: this.logarithmicScale ? "log" : "value",
name: this.unit,
min: this.fitYData ? this.minYAxis : undefined,
max: this.fitYData ? this.maxYAxis : undefined,
min: this._clampYAxis(minYAxis),
max: this._clampYAxis(maxYAxis),
position: rtl ? "right" : "left",
scale: true,
nameGap: 2,
nameTextStyle: {
align: "left",
},
splitLine: {
show: true,
lineStyle: splitLineStyle,
axisLine: {
show: false,
},
axisLabel: {
margin: 5,
@@ -218,6 +271,8 @@ export class StateHistoryChartLine extends LitElement {
} as YAXisOption,
legend: {
show: this.showNames,
type: "scroll",
animationDurationUpdate: 400,
icon: "circle",
padding: [20, 0],
},
@@ -227,37 +282,11 @@ export class StateHistoryChartLine extends LitElement {
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
bottom: 30,
},
visualMap: this._chartData
.map((_, seriesIndex) => {
const dataIndex = this._datasetToDataIndex[seriesIndex];
const data = this.data[dataIndex];
if (!data.statistics || data.statistics.length === 0) {
return false;
}
// render stat data with a slightly transparent line
const firstStateTS =
data.states[0]?.last_changed ?? this.endTime.getTime();
return {
show: false,
seriesIndex,
dimension: 0,
pieces: [
{
max: firstStateTS - 0.01,
colorAlpha: 0.5,
},
{
min: firstStateTS,
colorAlpha: 1,
},
],
};
})
.filter(Boolean) as VisualMapComponentOption[],
visualMap: this._visualMap,
tooltip: {
trigger: "axis",
appendTo: document.body,
formatter: this._renderTooltip.bind(this),
formatter: this._renderTooltip,
},
};
}
@@ -307,21 +336,28 @@ export class StateHistoryChartLine extends LitElement {
prevValues = datavalues;
};
const addDataSet = (nameY: string, color?: string, fill = false) => {
const addDataSet = (
id: string,
nameY: string,
color?: string,
fill = false
) => {
if (!color) {
color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++;
}
data.push({
id: nameY,
id,
data: [],
type: "line",
cursor: "default",
name: nameY,
color,
symbol: "circle",
step: "end",
symbolSize: 1,
step: "end",
sampling: "minmax",
animationDurationUpdate: 0,
lineStyle: {
width: fill ? 0 : 1.5,
},
@@ -375,13 +411,23 @@ export class StateHistoryChartLine extends LitElement {
entityState.attributes.target_temp_low
);
addDataSet(
`${this.hass.localize("ui.card.climate.current_temperature", {
name: name,
})}`
states.entity_id + "-current_temperature",
this.showNames
? this.hass.localize("ui.card.climate.current_temperature", {
name: name,
})
: this.hass.localize(
"component.climate.entity_component._.state_attributes.current_temperature.name"
)
);
if (hasHeat) {
addDataSet(
`${this.hass.localize("ui.card.climate.heating", { name: name })}`,
states.entity_id + "-heating",
this.showNames
? this.hass.localize("ui.card.climate.heating", { name: name })
: this.hass.localize(
"component.climate.entity_component._.state_attributes.hvac_action.state.heating"
),
computedStyles.getPropertyValue("--state-climate-heat-color"),
true
);
@@ -390,7 +436,12 @@ export class StateHistoryChartLine extends LitElement {
}
if (hasCool) {
addDataSet(
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
states.entity_id + "-cooling",
this.showNames
? this.hass.localize("ui.card.climate.cooling", { name: name })
: this.hass.localize(
"component.climate.entity_component._.state_attributes.hvac_action.state.cooling"
),
computedStyles.getPropertyValue("--state-climate-cool-color"),
true
);
@@ -400,22 +451,40 @@ export class StateHistoryChartLine extends LitElement {
if (hasTargetRange) {
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.high"),
})}`
states.entity_id + "-target_temperature_mode",
this.showNames
? this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.high"),
})
: this.hass.localize(
"component.climate.entity_component._.state_attributes.target_temp_high.name"
)
);
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.low"),
})}`
states.entity_id + "-target_temperature_mode_low",
this.showNames
? this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.low"),
})
: this.hass.localize(
"component.climate.entity_component._.state_attributes.target_temp_low.name"
)
);
} else {
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_entity", {
name: name,
})}`
states.entity_id + "-target_temperature",
this.showNames
? this.hass.localize(
"ui.card.climate.target_temperature_entity",
{
name: name,
}
)
: this.hass.localize(
"component.climate.entity_component._.state_attributes.temperature.name"
)
);
}
@@ -468,19 +537,29 @@ export class StateHistoryChartLine extends LitElement {
);
addDataSet(
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
name: name,
})}`
states.entity_id + "-target_humidity",
this.showNames
? this.hass.localize("ui.card.humidifier.target_humidity_entity", {
name: name,
})
: this.hass.localize(
"component.humidifier.entity_component._.state_attributes.humidity.name"
)
);
if (hasCurrent) {
addDataSet(
`${this.hass.localize(
"ui.card.humidifier.current_humidity_entity",
{
name: name,
}
)}`
states.entity_id + "-current_humidity",
this.showNames
? this.hass.localize(
"ui.card.humidifier.current_humidity_entity",
{
name: name,
}
)
: this.hass.localize(
"component.humidifier.entity_component._.state_attributes.current_humidity.name"
)
);
}
@@ -488,25 +567,40 @@ export class StateHistoryChartLine extends LitElement {
// If action attribute is not available, we shade the area when the device is on
if (hasHumidifying) {
addDataSet(
`${this.hass.localize("ui.card.humidifier.humidifying", {
name: name,
})}`,
states.entity_id + "-humidifying",
this.showNames
? this.hass.localize("ui.card.humidifier.humidifying", {
name: name,
})
: this.hass.localize(
"component.humidifier.entity_component._.state_attributes.action.state.humidifying"
),
computedStyles.getPropertyValue("--state-humidifier-on-color"),
true
);
} else if (hasDrying) {
addDataSet(
`${this.hass.localize("ui.card.humidifier.drying", {
name: name,
})}`,
states.entity_id + "-drying",
this.showNames
? this.hass.localize("ui.card.humidifier.drying", {
name: name,
})
: this.hass.localize(
"component.humidifier.entity_component._.state_attributes.action.state.drying"
),
computedStyles.getPropertyValue("--state-humidifier-on-color"),
true
);
} else {
addDataSet(
`${this.hass.localize("ui.card.humidifier.on_entity", {
name: name,
})}`,
states.entity_id + "-on",
this.showNames
? this.hass.localize("ui.card.humidifier.on_entity", {
name: name,
})
: this.hass.localize(
"component.humidifier.entity_component._.state.on"
),
undefined,
true
);
@@ -539,7 +633,7 @@ export class StateHistoryChartLine extends LitElement {
pushData(new Date(entityState.last_changed), series);
});
} else {
addDataSet(name);
addDataSet(states.entity_id, name);
let lastValue: number;
let lastDate: Date;
@@ -608,6 +702,46 @@ export class StateHistoryChartLine extends LitElement {
this._chartData = datasets;
this._entityIds = entityIds;
this._datasetToDataIndex = datasetToDataIndex;
const visualMap: VisualMapComponentOption[] = [];
this._chartData.forEach((_, seriesIndex) => {
const dataIndex = this._datasetToDataIndex[seriesIndex];
const data = this.data[dataIndex];
if (!data.statistics || data.statistics.length === 0) {
return;
}
// render stat data with a slightly transparent line
const firstStateTS =
data.states[0]?.last_changed ?? this.endTime.getTime();
visualMap.push({
show: false,
seriesIndex,
dimension: 0,
pieces: [
{
max: firstStateTS - 0.01,
colorAlpha: 0.5,
},
{
min: firstStateTS,
colorAlpha: 1,
},
],
});
});
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
}
private _clampYAxis(value?: number | ((values: any) => number)) {
if (this.logarithmicScale) {
// log(0) is -Infinity, so we need to set a minimum value
if (typeof value === "number") {
return Math.max(value, 0.1);
}
if (typeof value === "function") {
return (values: any) => Math.max(value(values), 0.1);
}
}
return value;
}
}
customElements.define("state-history-chart-line", StateHistoryChartLine);
@@ -8,7 +8,6 @@ import type {
TooltipFormatterCallback,
TooltipPositionCallbackParams,
} from "echarts/types/dist/shared";
import { differenceInDays } from "date-fns";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
import { computeRTL } from "../../common/util/compute_rtl";
@@ -22,7 +21,6 @@ import { luminosity } from "../../common/color/rgb";
import { hex2rgb } from "../../common/color/convert-color";
import { measureTextWidth } from "../../util/text";
import { fireEvent } from "../../common/dom/fire_event";
import { getTimeAxisLabelConfig } from "./axis-label";
@customElement("state-history-chart-timeline")
export class StateHistoryChartTimeline extends LitElement {
@@ -67,7 +65,7 @@ export class StateHistoryChartTimeline extends LitElement {
.hass=${this.hass}
.options=${this._chartOptions}
.height=${`${this.data.length * 30 + 30}px`}
.data=${this._chartData}
.data=${this._chartData as ECOption["series"]}
@chart-click=${this._handleChartClick}
></ha-chart-base>
`;
@@ -129,10 +127,12 @@ export class StateHistoryChartTimeline extends LitElement {
private _renderTooltip: TooltipFormatterCallback<TooltipPositionCallbackParams> =
(params: TooltipPositionCallbackParams) => {
const { value, name, marker } = Array.isArray(params)
const { value, name, marker, seriesName } = Array.isArray(params)
? params[0]
: params;
const title = `<h4 style="text-align: center; margin: 0;">${value![0]}</h4>`;
const title = seriesName
? `<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
: "";
const durationInMs = value![2] - value![1];
const formattedDuration = `${this.hass.localize(
"ui.components.history_charts.duration"
@@ -183,13 +183,12 @@ export class StateHistoryChartTimeline extends LitElement {
private _createOptions() {
const narrow = this.narrow;
const showNames = this.chunked || this.showNames;
const maxInternalLabelWidth = narrow ? 70 : 165;
const maxInternalLabelWidth = narrow ? 105 : 185;
const labelWidth = showNames
? Math.max(this.paddingYAxis, this._yWidth)
: 0;
const labelMargin = 5;
const rtl = computeRTL(this.hass);
const dayDifference = differenceInDays(this.endTime, this.startTime);
this._chartOptions = {
xAxis: {
type: "time",
@@ -197,21 +196,10 @@ export class StateHistoryChartTimeline extends LitElement {
max: this.endTime,
axisTick: {
show: true,
lineStyle: {
opacity: 0.4,
},
},
axisLabel: getTimeAxisLabelConfig(
this.hass.locale,
this.hass.config,
dayDifference
),
minInterval:
dayDifference >= 89 // quarter
? 28 * 3600 * 24 * 1000
: dayDifference > 2
? 3600 * 24 * 1000
: undefined,
splitLine: {
show: false,
},
},
yAxis: {
type: "category",
@@ -226,14 +214,18 @@ export class StateHistoryChartTimeline extends LitElement {
},
axisLabel: {
show: showNames,
width: labelWidth - labelMargin,
width: labelWidth,
overflow: "truncate",
margin: labelMargin,
formatter: (label: string) => {
const width = Math.min(
measureTextWidth(label, 12) + labelMargin,
maxInternalLabelWidth
);
formatter: (id: string) => {
const label = this._chartData.find((d) => d.id === id)
?.name as string;
const width = label
? Math.min(
measureTextWidth(label, 12) + labelMargin,
maxInternalLabelWidth
)
: 0;
if (width > this._yWidth) {
this._yWidth = width;
fireEvent(this, "y-width-changed", {
@@ -278,8 +270,9 @@ export class StateHistoryChartTimeline extends LitElement {
let prevState: string | null = null;
let locState: string | null = null;
let prevLastChanged = startTime;
const entityDisplay: string =
names[stateInfo.entity_id] || stateInfo.name;
const entityDisplay: string = this.showNames
? names[stateInfo.entity_id] || stateInfo.name || stateInfo.entity_id
: "";
const dataRow: unknown[] = [];
stateInfo.data.forEach((entityState) => {
@@ -307,7 +300,7 @@ export class StateHistoryChartTimeline extends LitElement {
);
dataRow.push({
value: [
entityDisplay,
stateInfo.entity_id,
prevLastChanged,
newLastChanged,
locState,
@@ -333,7 +326,7 @@ export class StateHistoryChartTimeline extends LitElement {
);
dataRow.push({
value: [
entityDisplay,
stateInfo.entity_id,
prevLastChanged,
endTime,
locState,
@@ -346,9 +339,10 @@ export class StateHistoryChartTimeline extends LitElement {
});
}
datasets.push({
id: stateInfo.entity_id,
data: dataRow,
name: entityDisplay,
dimensions: ["index", "start", "end", "name", "color", "textColor"],
dimensions: ["id", "start", "end", "name", "color", "textColor"],
type: "custom",
encode: {
x: [1, 2],
@@ -364,10 +358,10 @@ export class StateHistoryChartTimeline extends LitElement {
private _handleChartClick(e: CustomEvent<ECElementEvent>): void {
if (e.detail.targetType === "axisLabel") {
const dataset = this.data[e.detail.dataIndex];
const dataset = this._chartData[e.detail.dataIndex];
if (dataset) {
fireEvent(this, "hass-more-info", {
entityId: dataset.entity_id,
entityId: dataset.id as string,
});
}
}
+9 -2
View File
@@ -135,7 +135,7 @@ export class StateHistoryCharts extends LitElement {
return html``;
}
if (!Array.isArray(item)) {
return html`<div class="entry-container">
return html`<div class="entry-container line">
<state-history-chart-line
.hass=${this.hass}
.unit=${item.unit}
@@ -157,7 +157,7 @@ export class StateHistoryCharts extends LitElement {
></state-history-chart-line>
</div> `;
}
return html`<div class="entry-container">
return html`<div class="entry-container timeline">
<state-history-chart-timeline
.hass=${this.hass}
.data=${item}
@@ -299,6 +299,9 @@ export class StateHistoryCharts extends LitElement {
.entry-container {
width: 100%;
}
.entry-container.line {
flex: 1;
}
@@ -313,6 +316,10 @@ export class StateHistoryCharts extends LitElement {
padding-inline-end: 1px;
}
.entry-container.timeline:first-child {
margin-top: var(--timeline-top-margin);
}
.entry-container:not(:first-child) {
border-top: 2px solid var(--divider-color);
margin-top: 16px;
+139 -64
View File
@@ -1,15 +1,22 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type {
BarSeriesOption,
LineSeriesOption,
} from "echarts/types/dist/shared";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
formatNumber,
getNumberFormatOptions,
} from "../../common/number/format_number";
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
import { computeRTL } from "../../common/util/compute_rtl";
import type {
Statistics,
StatisticsMetaData,
@@ -21,16 +28,9 @@ import {
getStatisticMetadata,
statisticsHaveType,
} from "../../data/recorder";
import type { ECOption } from "../../resources/echarts";
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
import { computeRTL } from "../../common/util/compute_rtl";
import type { ECOption } from "../../resources/echarts";
import {
formatNumber,
getNumberFormatOptions,
} from "../../common/number/format_number";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { getTimeAxisLabelConfig } from "./axis-label";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean",
@@ -56,6 +56,8 @@ export class StatisticsChart extends LitElement {
@property() public unit?: string;
@property({ attribute: false }) public startTime?: Date;
@property({ attribute: false }) public endTime?: Date;
@property({ attribute: false, type: Array })
@@ -124,7 +126,10 @@ export class StatisticsChart extends LitElement {
changedProps.has("fitYData") ||
changedProps.has("logarithmicScale") ||
changedProps.has("hideLegend") ||
changedProps.has("_legendData")
changedProps.has("startTime") ||
changedProps.has("endTime") ||
changedProps.has("_legendData") ||
changedProps.has("_chartData")
) {
this._createOptions();
}
@@ -181,18 +186,31 @@ export class StatisticsChart extends LitElement {
this.requestUpdate("_hiddenStats");
}
private _renderTooltip = (params: any) =>
params
private _renderTooltip = (params: any) => {
const rendered: Record<string, boolean> = {};
const unit = this.unit
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
: "";
return params
.map((param, index: number) => {
if (rendered[param.seriesName]) return "";
rendered[param.seriesName] = true;
const statisticId = this._statisticIds[param.seriesIndex];
const stateObj = this.hass.states[statisticId];
const entry = this.hass.entities[statisticId];
// max series can have 3 values, as the second value is the max-min to form a band
const rawValue = String(param.value[2] ?? param.value[1]);
const options = getNumberFormatOptions(stateObj, entry) ?? {
maximumFractionDigits: 2,
};
const value = `${formatNumber(
// max series can have 3 values, as the second value is the max-min to form a band
(param.value[2] ?? param.value[1]) as number,
rawValue,
this.hass.locale,
getNumberFormatOptions(
undefined,
this.hass.entities[this._statisticIds[param.seriesIndex]]
)
)} ${this.unit}`;
options
)}${unit}`;
const time =
index === 0
@@ -202,36 +220,70 @@ export class StatisticsChart extends LitElement {
this.hass.config
) + "<br>"
: "";
return `${time}${param.marker} ${param.seriesName}: ${value}
`;
return `${time}${param.marker} ${param.seriesName}: ${value}`;
})
.filter(Boolean)
.join("<br>");
};
private _createOptions() {
const splitLineStyle = this.hass.themes?.darkMode ? { opacity: 0.15 } : {};
const dayDifference = this.daysToShow ?? 1;
let minYAxis: number | ((values: { min: number }) => number) | undefined =
this.minYAxis;
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
this.maxYAxis;
if (typeof minYAxis === "number") {
if (this.fitYData) {
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
}
} else if (this.logarithmicScale) {
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
}
if (typeof maxYAxis === "number") {
if (this.fitYData) {
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
}
} else if (this.logarithmicScale) {
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
}
const endTime = this.endTime ?? new Date();
let startTime = this.startTime;
if (!startTime) {
// set start time to the earliest point in the chart data
this._chartData.forEach((series) => {
if (!Array.isArray(series.data) || !series.data[0]) return;
const firstPoint = series.data[0] as any;
const timestamp = Array.isArray(firstPoint)
? firstPoint[0]
: firstPoint.value?.[0];
if (timestamp && (!startTime || new Date(timestamp) < startTime)) {
startTime = new Date(timestamp);
}
});
if (!startTime) {
// Calculate default start time based on dayDifference
startTime = new Date(
endTime.getTime() - dayDifference * 24 * 3600 * 1000
);
}
}
this._chartOptions = {
xAxis: {
type: "time",
axisLabel: getTimeAxisLabelConfig(
this.hass.locale,
this.hass.config,
dayDifference
),
axisLine: {
xAxis: [
{
id: "xAxis",
type: "time",
min: startTime,
max: this.endTime,
},
{
id: "hiddenAxis",
type: "time",
show: false,
},
splitLine: {
show: true,
lineStyle: splitLineStyle,
},
minInterval:
dayDifference >= 89 // quarter
? 28 * 3600 * 24 * 1000
: dayDifference > 2
? 3600 * 24 * 1000
: undefined,
},
],
yAxis: {
type: this.logarithmicScale ? "log" : "value",
name: this.unit,
@@ -240,24 +292,24 @@ export class StatisticsChart extends LitElement {
align: "left",
},
position: computeRTL(this.hass) ? "right" : "left",
// @ts-ignore
scale: this.chartType !== "bar",
min: this.fitYData ? undefined : this.minYAxis,
max: this.fitYData ? undefined : this.maxYAxis,
scale: true,
min: this._clampYAxis(minYAxis),
max: this._clampYAxis(maxYAxis),
splitLine: {
show: true,
lineStyle: splitLineStyle,
},
},
legend: {
show: !this.hideLegend,
type: "scroll",
animationDurationUpdate: 400,
icon: "circle",
padding: [20, 0],
data: this._legendData,
},
grid: {
...(this.hideLegend ? { top: this.unit ? 30 : 5 } : {}), // undefined is the same as 0
left: 20,
left: 1,
right: 1,
bottom: 0,
containLabel: true,
@@ -369,10 +421,12 @@ export class StatisticsChart extends LitElement {
) {
// if the end of the previous data doesn't match the start of the current data,
// we have to draw a gap so add a value at the end time, and then an empty value.
d.data!.push([prevEndTime, ...prevValues[i]!]);
d.data!.push(
this._transformDataValue([prevEndTime, ...prevValues[i]!])
);
d.data!.push([prevEndTime, null]);
}
d.data!.push([start, ...dataValues[i]!]);
d.data!.push(this._transformDataValue([start, ...dataValues[i]!]));
});
prevValues = dataValues;
prevEndTime = end;
@@ -421,9 +475,14 @@ export class StatisticsChart extends LitElement {
displayedLegend = displayedLegend || showLegend;
}
statTypes.push(type);
const borderColor =
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color;
const backgroundColor = band ? color + "3F" : color + "7F";
const series: LineSeriesOption | BarSeriesOption = {
id: `${statistic_id}-${type}`,
type: this.chartType,
smooth: this.chartType === "line" ? 0.4 : false,
smoothMonotone: "x",
cursor: "default",
data: [],
name: name
@@ -433,8 +492,9 @@ export class StatisticsChart extends LitElement {
: this.hass.localize(
`ui.components.statistics_charts.statistic_types.${type}`
),
symbol: "circle",
symbolSize: 0,
symbol: "none",
sampling: "minmax",
animationDurationUpdate: 0,
lineStyle: {
width: 1.5,
},
@@ -442,21 +502,15 @@ export class StatisticsChart extends LitElement {
this.chartType === "bar"
? {
borderRadius: [4, 4, 0, 0],
borderColor:
band && hasMean
? color + (this.hideLegend ? "00" : "7F")
: color,
borderColor,
borderWidth: 1.5,
}
: undefined,
color: band ? color + "3F" : color + "7F",
color: this.chartType === "bar" ? backgroundColor : borderColor,
};
if (band && this.chartType === "line") {
series.stack = `band-${statistic_id}`;
(series as LineSeriesOption).symbol = "none";
(series as LineSeriesOption).lineStyle = {
opacity: 0,
};
series.stackStrategy = "all";
if (drawBands && type === "max") {
(series as LineSeriesOption).areaStyle = {
color: color + "3F",
@@ -489,7 +543,7 @@ export class StatisticsChart extends LitElement {
}
} else if (type === "max" && this.chartType === "line") {
const max = stat.max || 0;
val.push(max - (stat.min || 0));
val.push(Math.abs(max - (stat.min || 0)));
val.push(max);
} else {
val.push(stat[type] ?? null);
@@ -518,6 +572,7 @@ export class StatisticsChart extends LitElement {
color,
type: this.chartType,
data: [],
xAxisIndex: 1,
});
});
@@ -529,6 +584,26 @@ export class StatisticsChart extends LitElement {
this._statisticIds = statisticIds;
}
private _transformDataValue(val: [Date, ...(number | null)[]]) {
if (this.chartType === "bar" && val[1] && val[1] < 0) {
return { value: val, itemStyle: { borderRadius: [0, 0, 4, 4] } };
}
return val;
}
private _clampYAxis(value?: number | ((values: any) => number)) {
if (this.logarithmicScale) {
// log(0) is -Infinity, so we need to set a minimum value
if (typeof value === "number") {
return Math.max(value, 0.1);
}
if (typeof value === "function") {
return (values: any) => Math.max(value(values), 0.1);
}
}
return value;
}
static styles = css`
:host {
display: block;
@@ -448,6 +448,7 @@ export class HaDataTable extends LitElement {
)}
@click=${this._handleHeaderClick}
.columnId=${key}
title=${ifDefined(column.title)}
>
${column.sortable
? html`
@@ -57,7 +57,7 @@ class HaEntityStatePicker extends LitElement {
(this._comboBox as any).items = [
...(this.extraOptions ?? []),
...(this.entityId && stateObj
? getStates(stateObj, this.attribute).map((key) => ({
? getStates(this.hass, stateObj, this.attribute).map((key) => ({
value: key,
label: !this.attribute
? this.hass.formatEntityState(stateObj, key)
+90 -8
View File
@@ -295,10 +295,12 @@ export class HaAssistChat extends LitElement {
this._addMessage(userMessage);
this.requestUpdate("_audioRecorder");
const hassMessage: AssistMessage = {
let hassMessage = {
who: "hass",
text: "…",
error: false,
};
let currentDeltaRole = "";
// To make sure the answer is placed at the right user text, we add it before we process it
try {
const unsub = await runAssistPipeline(
@@ -328,6 +330,43 @@ export class HaAssistChat extends LitElement {
this._addMessage(hassMessage);
}
if (event.type === "intent-progress") {
const delta = event.data.chat_log_delta;
// new message
if (delta.role) {
// If currentDeltaRole exists, it means we're receiving our
// second or later message. Let's add it to the chat.
if (currentDeltaRole && delta.role && hassMessage.text !== "…") {
// Remove progress indicator of previous message
hassMessage.text = hassMessage.text.substring(
0,
hassMessage.text.length - 1
);
hassMessage = {
who: "hass",
text: "…",
error: false,
};
this._addMessage(hassMessage);
}
currentDeltaRole = delta.role;
}
if (
currentDeltaRole === "assistant" &&
"content" in delta &&
delta.content
) {
hassMessage.text =
hassMessage.text.substring(0, hassMessage.text.length - 1) +
delta.content +
"…";
this.requestUpdate("_conversation");
}
}
if (event.type === "intent-end") {
this._conversationId = event.data.intent_output.conversation_id;
const plain = event.data.intent_output.response.speech?.plain;
@@ -435,28 +474,71 @@ export class HaAssistChat extends LitElement {
this._processing = true;
this._audio?.pause();
this._addMessage({ who: "user", text });
const message: AssistMessage = {
let hassMessage = {
who: "hass",
text: "…",
error: false,
};
let currentDeltaRole = "";
// To make sure the answer is placed at the right user text, we add it before we process it
this._addMessage(message);
this._addMessage(hassMessage);
try {
const unsub = await runAssistPipeline(
this.hass,
(event) => {
if (event.type === "intent-progress") {
const delta = event.data.chat_log_delta;
// new message and previous message has content
if (delta.role) {
// If currentDeltaRole exists, it means we're receiving our
// second or later message. Let's add it to the chat.
if (
currentDeltaRole &&
delta.role === "assistant" &&
hassMessage.text !== "…"
) {
// Remove progress indicator of previous message
hassMessage.text = hassMessage.text.substring(
0,
hassMessage.text.length - 1
);
hassMessage = {
who: "hass",
text: "…",
error: false,
};
this._addMessage(hassMessage);
}
currentDeltaRole = delta.role;
}
if (
currentDeltaRole === "assistant" &&
"content" in delta &&
delta.content
) {
hassMessage.text =
hassMessage.text.substring(0, hassMessage.text.length - 1) +
delta.content +
"…";
this.requestUpdate("_conversation");
}
}
if (event.type === "intent-end") {
this._conversationId = event.data.intent_output.conversation_id;
const plain = event.data.intent_output.response.speech?.plain;
if (plain) {
message.text = plain.speech;
hassMessage.text = plain.speech;
}
this.requestUpdate("_conversation");
unsub();
}
if (event.type === "error") {
message.text = event.data.message;
message.error = true;
hassMessage.text = event.data.message;
hassMessage.error = true;
this.requestUpdate("_conversation");
unsub();
}
@@ -470,8 +552,8 @@ export class HaAssistChat extends LitElement {
}
);
} catch {
message.text = this.hass.localize("ui.dialogs.voice_command.error");
message.error = true;
hassMessage.text = this.hass.localize("ui.dialogs.voice_command.error");
hassMessage.error = true;
this.requestUpdate("_conversation");
} finally {
this._processing = false;
+1 -4
View File
@@ -329,15 +329,12 @@ export class HaBaseTimeInput extends LitElement {
:host([clearable]) {
position: relative;
}
:host {
display: block;
}
.time-input-wrap-wrap {
display: flex;
}
.time-input-wrap {
display: flex;
flex: 1;
flex: var(--time-input-flex, unset);
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
overflow: hidden;
position: relative;
+110
View File
@@ -0,0 +1,110 @@
import { customElement, property, state } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
import "./ha-button";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-textfield";
import type { HomeAssistant } from "../types";
import { copyToClipboard } from "../common/util/copy-clipboard";
import { showToast } from "../util/toast";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-copy-textfield")
export class HaCopyTextfield extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "value" }) public value!: string;
@property({ attribute: "masked-value" }) public maskedValue?: string;
@property({ attribute: "label" }) public label?: string;
@state() private _showMasked = true;
public render() {
return html`
<div class="container">
<div class="textfield-container">
<ha-textfield
.value=${this._showMasked && this.maskedValue
? this.maskedValue
: this.value}
readonly
.suffix=${this.maskedValue
? html`<div style="width: 24px"></div>`
: nothing}
@click=${this._focusInput}
></ha-textfield>
${this.maskedValue
? html`<ha-icon-button
class="toggle-unmasked"
.label=${this.hass.localize(
`ui.common.${this._showMasked ? "show" : "hide"}`
)}
@click=${this._toggleMasked}
.path=${this._showMasked ? mdiEye : mdiEyeOff}
></ha-icon-button>`
: nothing}
</div>
<ha-button @click=${this._copy} unelevated>
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
${this.label || this.hass.localize("ui.common.copy")}
</ha-button>
</div>
`;
}
private _focusInput(ev) {
const inputElement = ev.currentTarget as HaTextField;
inputElement.select();
}
private _toggleMasked(): void {
this._showMasked = !this._showMasked;
}
private async _copy(): Promise<void> {
await copyToClipboard(this.value);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static styles = css`
.container {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.textfield-container {
position: relative;
flex: 1;
}
.textfield-container ha-textfield {
display: block;
}
.toggle-unmasked {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-copy-textfield": HaCopyTextfield;
}
}
+174 -36
View File
@@ -5,6 +5,7 @@ import "@material/mwc-list/mwc-list-item";
import { mdiCalendar } from "@mdi/js";
import {
addDays,
subHours,
endOfDay,
endOfMonth,
endOfWeek,
@@ -15,6 +16,7 @@ import {
startOfYear,
isThisYear,
} from "date-fns";
import { fromZonedTime, toZonedTime } from "date-fns-tz";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -22,16 +24,18 @@ import { ifDefined } from "lit/directives/if-defined";
import { calcDate, shiftDateRange } from "../common/datetime/calc_date";
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import {
formatShortDateTimeWithYear,
formatShortDateTime,
formatShortDateTimeWithYear,
} from "../common/datetime/format_date_time";
import { useAmPm } from "../common/datetime/use_am_pm";
import { fireEvent } from "../common/dom/fire_event";
import { TimeZone } from "../data/translation";
import type { HomeAssistant } from "../types";
import "./date-range-picker";
import "./ha-icon-button";
import "./ha-textarea";
import "./ha-icon-button-next";
import "./ha-icon-button-prev";
import "./ha-textarea";
export type DateRangePickerRanges = Record<string, [Date, Date]>;
@@ -175,6 +179,96 @@ export class HaDateRangePicker extends LitElement {
weekStartsOn,
}),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-1h"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
1
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-12h"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
12
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-24h"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
24
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-7d"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
24 * 7
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-30d"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
24 * 30
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
}
: {}),
};
@@ -197,14 +291,15 @@ export class HaDateRangePicker extends LitElement {
?auto-apply=${this.autoApply}
time-picker=${this.timePicker}
twentyfour-hours=${this._hour24format}
start-date=${this.startDate.toISOString()}
end-date=${this.endDate.toISOString()}
start-date=${this._formatDate(this.startDate)}
end-date=${this._formatDate(this.endDate)}
?ranges=${this.ranges !== false}
opening-direction=${ifDefined(
this.openingDirection || this._calcedOpeningDirection
)}
first-day=${firstWeekdayIndex(this.hass.locale)}
language=${this.hass.locale.language}
@change=${this._handleChange}
>
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
${!this.minimal
@@ -325,9 +420,31 @@ export class HaDateRangePicker extends LitElement {
}
private _applyDateRange() {
if (this.hass.locale.time_zone === TimeZone.server) {
const dateRangePicker = this._dateRangePicker;
const startDate = fromZonedTime(
dateRangePicker.start,
this.hass.config.time_zone
);
const endDate = fromZonedTime(
dateRangePicker.end,
this.hass.config.time_zone
);
dateRangePicker.clickRange([startDate, endDate]);
}
this._dateRangePicker.clickedApply();
}
private _formatDate(date: Date): string {
if (this.hass.locale.time_zone === TimeZone.server) {
return toZonedTime(date, this.hass.config.time_zone).toISOString();
}
return date.toISOString();
}
private get _dateRangePicker() {
const dateRangePicker = this.shadowRoot!.querySelector(
"date-range-picker"
@@ -358,45 +475,66 @@ export class HaDateRangePicker extends LitElement {
}
}
private _handleChange(ev: CustomEvent) {
ev.stopPropagation();
const startDate = ev.detail.startDate;
const endDate = ev.detail.endDate;
fireEvent(this, "value-changed", {
value: { startDate, endDate },
});
}
static styles = css`
ha-icon-button {
direction: var(--direction);
}
ha-icon-button {
direction: var(--direction);
.date-range-inputs {
display: flex;
align-items: center;
gap: 8px;
}
.date-range-ranges {
border-right: 1px solid var(--divider-color);
}
.date-range-footer {
display: flex;
justify-content: flex-end;
padding: 8px;
border-top: 1px solid var(--divider-color);
}
ha-textarea {
display: inline-block;
width: 340px;
}
@media only screen and (max-width: 460px) {
ha-textarea {
width: 100%;
}
.date-range-inputs {
display: flex;
align-items: center;
gap: 8px;
}
}
@media only screen and (max-width: 800px) {
.date-range-ranges {
border-right: 1px solid var(--divider-color);
border-right: none;
border-bottom: 1px solid var(--divider-color);
}
}
@media only screen and (max-height: 940px) and (max-width: 800px) {
.date-range-ranges {
overflow: auto;
max-height: calc(70vh - 330px);
min-height: 160px;
}
.date-range-footer {
display: flex;
justify-content: flex-end;
padding: 8px;
border-top: 1px solid var(--divider-color);
:host([header-position]) .date-range-ranges {
max-height: calc(90vh - 430px);
}
ha-textarea {
display: inline-block;
width: 340px;
}
@media only screen and (max-width: 460px) {
ha-textarea {
width: 100%
}
@media only screen and (max-width: 800px) {
.date-range-ranges {
border-right: none;
border-bottom: 1px solid var(--divider-color);
}
}
`;
}
`;
}
declare global {
+24 -18
View File
@@ -11,6 +11,7 @@ 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";
import type { LocalizeFunc } from "../common/translations/localize";
declare global {
interface HASSDomEvents {
@@ -23,6 +24,8 @@ declare global {
export class HaFileUpload extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize?: LocalizeFunc;
@property() public accept!: string;
@property() public icon?: string;
@@ -31,6 +34,10 @@ export class HaFileUpload extends LitElement {
@property() public secondary?: string;
@property({ attribute: "uploading-label" }) public uploadingLabel?: string;
@property({ attribute: "delete-label" }) public deleteLabel?: string;
@property() public supports?: string;
@property({ type: Object }) public value?: File | File[] | FileList | string;
@@ -73,23 +80,22 @@ export class HaFileUpload extends LitElement {
}
public render(): TemplateResult {
const localize = this.localize || this.hass!.localize;
return html`
${this.uploading
? html`<div class="container">
<div class="uploading">
<span class="header"
>${this.value
? this.hass?.localize(
"ui.components.file-upload.uploading_name",
{ name: this._name }
)
: this.hass?.localize(
"ui.components.file-upload.uploading"
)}</span
>${this.uploadingLabel || this.value
? localize("ui.components.file-upload.uploading_name", {
name: this._name,
})
: localize("ui.components.file-upload.uploading")}</span
>
${this.progress
? html`<div class="progress">
${this.progress}${blankBeforePercent(this.hass!.locale)}%
${this.progress}${this.hass &&
blankBeforePercent(this.hass!.locale)}%
</div>`
: nothing}
</div>
@@ -116,14 +122,11 @@ export class HaFileUpload extends LitElement {
.path=${this.icon || mdiFileUpload}
></ha-svg-icon>
<ha-button unelevated @click=${this._openFilePicker}>
${this.label ||
this.hass?.localize("ui.components.file-upload.label")}
${this.label || localize("ui.components.file-upload.label")}
</ha-button>
<span class="secondary"
>${this.secondary ||
this.hass?.localize(
"ui.components.file-upload.secondary"
)}</span
localize("ui.components.file-upload.secondary")}</span
>
<span class="supports">${this.supports}</span>`
: typeof this.value === "string"
@@ -136,8 +139,7 @@ export class HaFileUpload extends LitElement {
</div>
<ha-icon-button
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.delete") ||
"Delete"}
.label=${this.deleteLabel || localize("ui.common.delete")}
.path=${mdiDelete}
></ha-icon-button>
</div>`
@@ -155,8 +157,8 @@ export class HaFileUpload extends LitElement {
</div>
<ha-icon-button
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.delete") ||
"Delete"}
.label=${this.deleteLabel ||
localize("ui.common.delete")}
.path=${mdiDelete}
></ha-icon-button>
</div>`
@@ -238,6 +240,10 @@ export class HaFileUpload extends LitElement {
border-radius: var(--mdc-shape-small, 4px);
height: 100%;
}
.row {
display: flex;
align-items: center;
}
label.container {
border: dashed 1px
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
-1
View File
@@ -80,7 +80,6 @@ export class HaFormString extends LitElement implements HaFormElement {
if (!this.isPassword) return nothing;
return html`
<ha-icon-button
toggles
.label=${this.localize?.(
`${this.localizeBaseKey}.${
this.unmaskedPassword ? "hide_password" : "show_password"
+33
View File
@@ -1,7 +1,12 @@
import type { PropertyValues } from "lit";
import { ReactiveElement } from "lit";
import { customElement, property } from "lit/decorators";
import hash from "object-hash";
import { fireEvent } from "../common/dom/fire_event";
import { renderMarkdown } from "../resources/render-markdown";
import { CacheManager } from "../util/cache-manager";
const markdownCache = new CacheManager<string>(1000);
const _gitHubMarkdownAlerts = {
reType:
@@ -26,6 +31,16 @@ class HaMarkdownElement extends ReactiveElement {
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
false;
@property({ type: Boolean }) public cache = false;
public disconnectedCallback() {
super.disconnectedCallback();
if (this.cache) {
const key = this._computeCacheKey();
markdownCache.set(key, this.innerHTML);
}
}
protected createRenderRoot() {
return this;
}
@@ -37,6 +52,24 @@ class HaMarkdownElement extends ReactiveElement {
}
}
protected willUpdate(_changedProperties: PropertyValues): void {
if (!this.innerHTML && this.cache) {
const key = this._computeCacheKey();
if (markdownCache.has(key)) {
this.innerHTML = markdownCache.get(key)!;
this._resize();
}
}
}
private _computeCacheKey() {
return hash({
content: this.content,
allowSvg: this.allowSvg,
breaks: this.breaks,
});
}
private async _render() {
this.innerHTML = await renderMarkdown(
String(this.content),
+3
View File
@@ -13,6 +13,8 @@ export class HaMarkdown extends LitElement {
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
false;
@property({ type: Boolean }) public cache = false;
protected render() {
if (!this.content) {
return nothing;
@@ -23,6 +25,7 @@ export class HaMarkdown extends LitElement {
.allowSvg=${this.allowSvg}
.breaks=${this.breaks}
.lazyImages=${this.lazyImages}
.cache=${this.cache}
></ha-markdown-element>`;
}
+13 -6
View File
@@ -64,9 +64,13 @@ export class HaNetwork extends LitElement {
>
</ha-checkbox>
</span>
<span slot="heading" data-for="auto_configure"> Auto Configure </span>
<span slot="heading" data-for="auto_configure">
${this.hass.localize(
"ui.panel.config.network.adapter.auto_configure"
)}
</span>
<span slot="description" data-for="auto_configure">
Detected:
${this.hass.localize("ui.panel.config.network.adapter.detected")}:
${format_auto_detected_interfaces(this.networkConfig.adapters)}
</span>
</ha-settings-row>
@@ -85,18 +89,21 @@ export class HaNetwork extends LitElement {
</ha-checkbox>
</span>
<span slot="heading">
Adapter: ${adapter.name}
${this.hass.localize(
"ui.panel.config.network.adapter.adapter"
)}:
${adapter.name}
${adapter.default
? html`<ha-svg-icon .path=${mdiStar}></ha-svg-icon>
(Default)`
: ""}
(${this.hass.localize("ui.common.default")})`
: nothing}
</span>
<span slot="description">
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
</span>
</ha-settings-row>`
)
: ""}
: nothing}
`;
}
-1
View File
@@ -132,7 +132,6 @@ export class HaPasswordField extends LitElement {
@change=${this._handleChangeEvent}
></ha-textfield>
<ha-icon-button
toggles
.label=${this.hass?.localize(
this._unmaskedPassword
? "ui.components.selectors.text.hide_password"
+9 -7
View File
@@ -8,7 +8,7 @@ import { customElement, property, query, state } from "lit/decorators";
// and "qr-scanner" defaults to a suboptimal implementation if it is not available.
// The following import makes a better implementation available that is based on a
// WebAssembly port of ZXing:
import { setZXingModuleOverrides } from "barcode-detector";
import { prepareZXingModule } from "barcode-detector";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
@@ -21,12 +21,14 @@ import "./ha-list-item";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
setZXingModuleOverrides({
locateFile: (path: string, prefix: string) => {
if (path.endsWith(".wasm")) {
return "/static/js/zxing_reader.wasm";
}
return prefix + path;
prepareZXingModule({
overrides: {
locateFile: (path: string, prefix: string) => {
if (path.endsWith(".wasm")) {
return "/static/js/zxing_reader.wasm";
}
return prefix + path;
},
},
});
@@ -95,7 +95,6 @@ export class HaTextSelector extends LitElement {
></ha-textfield>
${this.selector.text?.type === "password"
? html`<ha-icon-button
toggles
.label=${this.hass?.localize(
this._unmaskedPassword
? "ui.components.selectors.text.hide_password"
+198 -51
View File
@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@shoelace-style/shoelace/dist/components/skeleton/skeleton";
import {
mdiBell,
mdiCalendar,
@@ -49,6 +50,10 @@ import "./ha-sortable";
import "./ha-svg-icon";
import "./user/ha-user-badge";
import { preventDefault } from "../common/dom/prevent_default";
import {
saveSidebarPreferences,
subscribeSidebarPreferences,
} from "../data/sidebar";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
@@ -207,30 +212,40 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
@storage({
key: "sidebarPanelOrder",
state: true,
subscribe: true,
})
private _panelOrder: string[] = [];
@storage({ key: "sidebarPanelOrder", state: true, subscribe: true })
private _devicePanelOrder?: string[];
@storage({
key: "sidebarHiddenPanels",
state: true,
subscribe: true,
})
private _hiddenPanels: string[] = [];
@storage({ key: "sidebarHiddenPanels", state: true, subscribe: true })
private _deviceHiddenPanels?: string[];
@state()
private _userPanelOrder: string[] = [];
@state()
private _userHiddenPanels: string[] = [];
@state()
private _loadingUserPreferences = true;
public hassSubscribe(): UnsubscribeFunc[] {
return this.hass.user?.is_admin
? [
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._issuesCount = repairs.issues.filter(
(issue) => !issue.ignored
).length;
}),
]
: [];
const subscribeFunctions = [
subscribeSidebarPreferences(this.hass, (sidebar) => {
this._userPanelOrder = sidebar?.panelOrder || [];
this._userHiddenPanels = sidebar?.hiddenPanels || [];
this._loadingUserPreferences = false;
}),
];
if (this.hass.user?.is_admin) {
subscribeFunctions.push(
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._issuesCount = repairs.issues.filter(
(issue) => !issue.ignored
).length;
})
);
}
return subscribeFunctions;
}
protected render() {
@@ -260,8 +275,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
changedProps.has("_updatesCount") ||
changedProps.has("_issuesCount") ||
changedProps.has("_notifications") ||
changedProps.has("_hiddenPanels") ||
changedProps.has("_panelOrder")
changedProps.has("_devicePanelOrder") ||
changedProps.has("_deviceHiddenPanels") ||
changedProps.has("_userPanelOrder") ||
changedProps.has("_userHiddenPanels")
) {
return true;
}
@@ -381,12 +398,51 @@ class HaSidebar extends SubscribeMixin(LitElement) {
</div>`;
}
private _getPanelPreferencesMemoized = memoizeOne(
(
userPanelOrder: string[],
userHiddenPanels: string[],
userPreferencesLoading: boolean,
devicePanelOrder?: string[],
deviceHiddenPanels?: string[]
): { panelOrder: string[]; hiddenPanels: string[]; loading: boolean } => {
let panelOrder = userPanelOrder ?? [];
let hiddenPanels = userHiddenPanels ?? [];
let loading = userPreferencesLoading;
if (devicePanelOrder || deviceHiddenPanels) {
panelOrder = devicePanelOrder ?? [];
hiddenPanels = deviceHiddenPanels ?? [];
loading = false;
}
return {
panelOrder,
hiddenPanels,
loading,
};
}
);
private _getPanelPreferences() {
return this._getPanelPreferencesMemoized(
this._userPanelOrder,
this._userHiddenPanels,
this._loadingUserPreferences,
this._devicePanelOrder,
this._deviceHiddenPanels
);
}
private _renderAllPanels() {
const { panelOrder, hiddenPanels, loading } = this._getPanelPreferences();
const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels,
panelOrder,
hiddenPanels,
this.hass.locale
);
@@ -407,12 +463,21 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@keydown=${this._listboxKeydown}
@iron-activate=${preventDefault}
>
${this.editMode
? this._renderPanelsEdit(beforeSpacer)
: this._renderPanels(beforeSpacer)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
${loading ? html`
<div class="loading">
<sl-skeleton effect="sheen"></sl-skeleton>
<sl-skeleton effect="sheen"></sl-skeleton>
<sl-skeleton effect="sheen"></sl-skeleton>
<sl-skeleton effect="sheen"></sl-skeleton>
</div>
` : html`
${this.editMode
? this._renderPanelsEdit(beforeSpacer)
: this._renderPanels(beforeSpacer)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
`}
</paper-listbox>
`;
}
@@ -474,23 +539,49 @@ class HaSidebar extends SubscribeMixin(LitElement) {
`;
}
private async _setPanelOrder(panelOrder: string[]) {
if (this._devicePanelOrder || this._deviceHiddenPanels) {
this._devicePanelOrder = [...panelOrder];
} else {
this._userPanelOrder = [...panelOrder];
await saveSidebarPreferences(this.hass, {
panelOrder: panelOrder,
hiddenPanels: this._userHiddenPanels,
});
}
}
private async _setHiddenPanels(hiddenPanels: string[]) {
if (this._devicePanelOrder || this._deviceHiddenPanels) {
this._deviceHiddenPanels = hiddenPanels;
} else {
this._userHiddenPanels = hiddenPanels;
await saveSidebarPreferences(this.hass, {
panelOrder: this._userPanelOrder,
hiddenPanels: hiddenPanels,
});
}
}
private _panelMoved(ev: CustomEvent) {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const { panelOrder, hiddenPanels } = this._getPanelPreferences();
const [beforeSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels,
panelOrder,
hiddenPanels!,
this.hass.locale
);
const panelOrder = beforeSpacer.map((panel) => panel.url_path);
const panel = panelOrder.splice(oldIndex, 1)[0];
panelOrder.splice(newIndex, 0, panel);
const panelOrderNew = beforeSpacer.map((panel) => panel.url_path);
const panel = panelOrderNew.splice(oldIndex, 1)[0];
panelOrderNew.splice(newIndex, 0, panel);
this._panelOrder = panelOrder;
this._setPanelOrder(panelOrderNew);
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
@@ -507,8 +598,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
private _renderHiddenPanels() {
return html`${this._hiddenPanels.length
? html`${this._hiddenPanels.map((url) => {
const { hiddenPanels } = this._getPanelPreferences();
return html`${hiddenPanels.length
? html`${hiddenPanels.map((url) => {
const panel = this.hass.panels[url];
if (!panel) {
return "";
@@ -690,9 +783,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
}
private get _tooltip() {
@@ -730,21 +821,25 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private async _hidePanel(ev: Event) {
ev.preventDefault();
const panel = (ev.currentTarget as any).panel;
if (this._hiddenPanels.includes(panel)) {
if ((this._deviceHiddenPanels || this._userHiddenPanels).includes(panel)) {
return;
}
const { panelOrder, hiddenPanels } = this._getPanelPreferences();
// Make a copy for Memoize
this._hiddenPanels = [...this._hiddenPanels, panel];
this._setHiddenPanels([...hiddenPanels, panel]);
// Remove it from the panel order
this._panelOrder = this._panelOrder.filter((order) => order !== panel);
this._setPanelOrder(panelOrder.filter((order) => order !== panel));
}
private async _unhidePanel(ev: Event) {
ev.preventDefault();
const panel = (ev.currentTarget as any).panel;
this._hiddenPanels = this._hiddenPanels.filter(
(hidden) => hidden !== panel
);
const { hiddenPanels } = this._getPanelPreferences();
this._setHiddenPanels(hiddenPanels.filter((hidden) => hidden !== panel));
}
private _itemMouseEnter(ev: MouseEvent) {
@@ -784,9 +879,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._hideTooltip();
}
@eventOptions({
passive: true,
})
@eventOptions({ passive: true })
private _listboxScroll() {
// On keypresses on the listbox, we're going to ignore scroll events
// for 100ms so that if pressing down arrow scrolls the sidebar, the tooltip
@@ -1117,6 +1210,60 @@ class HaSidebar extends SubscribeMixin(LitElement) {
-webkit-transform: scaleX(var(--scale-direction));
transform: scaleX(var(--scale-direction));
}
@keyframes skeletonAnimate {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes contentAnimate {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.loading {
opacity: 0;
animation-name: skeletonAnimate;
animation-duration: 2000ms;
animation-delay: 0;
animation-iteration-count: 1;
animation-fill-mode: forwards;
animation-timing-function: ease-out;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
sl-skeleton {
--border-radius: 8px;
height: 24px;
--color: var(--outline-color);
--sheen-color: var(--outline-hover-color);
}
sl-skeleton:nth-child(2) {
width: 70%;
}
sl-skeleton:nth-child(3) {
width: 30%;
}
sl-skeleton:nth-child(4) {
width: 90%;
}
`,
];
}
+51 -26
View File
@@ -8,9 +8,10 @@ import type {
Map,
Marker,
Polyline,
MarkerClusterGroup,
} from "leaflet";
import type { PropertyValues } from "lit";
import { ReactiveElement, css } from "lit";
import { css, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { formatDateTime } from "../../common/datetime/format_date_time";
@@ -26,6 +27,7 @@ import type { HomeAssistant, ThemeMode } from "../../types";
import { isTouch } from "../../util/is_touch";
import "../ha-icon-button";
import "./ha-entity-marker";
import { DecoratedMarker } from "../../common/map/decorated_marker";
declare global {
// for fire event
@@ -84,6 +86,9 @@ export class HaMap extends ReactiveElement {
@property({ type: Number }) public zoom = 14;
@property({ attribute: "cluster-markers", type: Boolean })
public clusterMarkers = true;
@state() private _loaded = false;
public leafletMap?: Map;
@@ -96,10 +101,12 @@ export class HaMap extends ReactiveElement {
private _mapFocusItems: (Marker | Circle)[] = [];
private _mapZones: (Marker | Circle)[] = [];
private _mapZones: DecoratedMarker[] = [];
private _mapFocusZones: (Marker | Circle)[] = [];
private _mapCluster: MarkerClusterGroup | undefined;
private _mapPaths: (Polyline | CircleMarker)[] = [];
private _clickCount = 0;
@@ -151,6 +158,10 @@ export class HaMap extends ReactiveElement {
}
}
if (changedProps.has("clusterMarkers")) {
this._drawEntities();
}
if (changedProps.has("_loaded") || changedProps.has("paths")) {
this._drawPaths();
}
@@ -175,6 +186,7 @@ export class HaMap extends ReactiveElement {
) {
return;
}
this._updateMapStyle();
}
@@ -426,6 +438,11 @@ export class HaMap extends ReactiveElement {
this._mapFocusZones = [];
}
if (this._mapCluster) {
this._mapCluster.remove();
this._mapCluster = undefined;
}
if (!this.entities) {
return;
}
@@ -481,26 +498,24 @@ export class HaMap extends ReactiveElement {
iconHTML = el.outerHTML;
}
// create marker with the icon
this._mapZones.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className,
}),
interactive: this.interactiveZones,
title,
})
);
// create circle around it
const circle = Leaflet.circle([latitude, longitude], {
interactive: false,
color: passive ? passiveZoneColor : zoneColor,
radius,
});
this._mapZones.push(circle);
const marker = new DecoratedMarker([latitude, longitude], circle, {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className,
}),
interactive: this.interactiveZones,
title,
});
this._mapZones.push(marker);
if (
this.fitZones &&
(typeof entity === "string" || entity.focus !== false)
@@ -538,7 +553,7 @@ export class HaMap extends ReactiveElement {
}
// create marker with the icon
const marker = Leaflet.marker([latitude, longitude], {
const marker = new DecoratedMarker([latitude, longitude], undefined, {
icon: Leaflet.divIcon({
html: entityMarker,
iconSize: [48, 48],
@@ -546,24 +561,34 @@ export class HaMap extends ReactiveElement {
}),
title: title,
});
this._mapItems.push(marker);
if (typeof entity === "string" || entity.focus !== false) {
this._mapFocusItems.push(marker);
}
// create circle around if entity has accuracy
if (gpsAccuracy) {
this._mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: darkPrimaryColor,
radius: gpsAccuracy,
})
);
marker.decorationLayer = Leaflet.circle([latitude, longitude], {
interactive: false,
color: darkPrimaryColor,
radius: gpsAccuracy,
});
}
this._mapItems.push(marker);
}
if (this.clusterMarkers) {
this._mapCluster = Leaflet.markerClusterGroup({
showCoverageOnHover: false,
removeOutsideVisibleBounds: false,
maxClusterRadius: 40,
});
this._mapCluster.addLayers(this._mapItems);
map.addLayer(this._mapCluster);
} else {
this._mapItems.forEach((marker) => map.addLayer(marker));
}
this._mapItems.forEach((marker) => map.addLayer(marker));
this._mapZones.forEach((marker) => map.addLayer(marker));
}
+72 -19
View File
@@ -1,25 +1,81 @@
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import "../ha-icon";
import "../ha-svg-icon";
import { classMap } from "lit/directives/class-map";
export type TileIconImageStyle = "square" | "rounded-square" | "circle";
export const DEFAULT_TILE_ICON_BORDER_STYLE = "circle";
@customElement("ha-tile-icon")
export class HaTileIcon extends LitElement {
@property({ type: Boolean, reflect: true })
public interactive = false;
@property({ attribute: "border-style", type: String })
public imageStyle?: TileIconImageStyle;
@property({ attribute: false })
public imageUrl?: string;
protected render(): TemplateResult {
return html`
<div class="shape">
if (this.imageUrl) {
const imageStyle = this.imageStyle || DEFAULT_TILE_ICON_BORDER_STYLE;
return html`
<div class="container ${classMap({ [imageStyle]: this.imageUrl })}">
<img alt="" src=${this.imageUrl} />
</div>
<slot></slot>
`;
}
return html`
<div class="container ${this.interactive ? "background" : ""}">
<slot name="icon"></slot>
</div>
<slot></slot>
`;
}
static styles = css`
:host {
--tile-icon-color: var(--disabled-color);
--mdc-icon-size: 22px;
--tile-icon-opacity: 0.2;
--tile-icon-hover-opacity: 0.35;
--mdc-icon-size: 24px;
position: relative;
user-select: none;
transition: transform 180ms ease-in-out;
}
.shape::before {
:host([interactive]:active) {
transform: scale(1.2);
}
:host([interactive]:hover) {
--tile-icon-opacity: var(--tile-icon-hover-opacity);
}
.container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 18px;
overflow: hidden;
transition: box-shadow 180ms ease-in-out;
}
:host([interactive]:focus-visible) .container {
box-shadow: 0 0 0 2px var(--tile-icon-color);
}
.container.rounded-square {
border-radius: 8px;
}
.container.square {
border-radius: 0;
}
.container.background::before {
content: "";
position: absolute;
top: 0;
@@ -27,24 +83,21 @@ export class HaTileIcon extends LitElement {
height: 100%;
width: 100%;
background-color: var(--tile-icon-color);
transition: background-color 180ms ease-in-out;
opacity: 0.2;
transition:
background-color 180ms ease-in-out,
opacity 180ms ease-in-out;
opacity: var(--tile-icon-opacity);
}
.shape {
position: relative;
width: 36px;
height: 36px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: color 180ms ease-in-out;
overflow: hidden;
}
.shape ::slotted(*) {
.container ::slotted([slot="icon"]) {
display: flex;
color: var(--tile-icon-color);
transition: color 180ms ease-in-out;
pointer-events: none;
}
.container img {
width: 100%;
height: 100%;
object-fit: cover;
}
`;
}
-53
View File
@@ -1,53 +0,0 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
export type TileImageStyle = "square" | "rounded-square" | "circle";
@customElement("ha-tile-image")
export class HaTileImage extends LitElement {
@property({ attribute: false }) public imageUrl?: string;
@property({ attribute: false }) public imageAlt?: string;
@property({ attribute: false }) public imageStyle: TileImageStyle = "circle";
protected render() {
return html`
<div class="image ${this.imageStyle}">
${this.imageUrl
? html`<img alt=${ifDefined(this.imageAlt)} src=${this.imageUrl} />`
: nothing}
</div>
`;
}
static styles = css`
.image {
position: relative;
width: 36px;
height: 36px;
border-radius: 18px;
display: flex;
flex: none;
align-items: center;
justify-content: center;
overflow: hidden;
}
.image.rounded-square {
border-radius: 8%;
}
.image.square {
border-radius: 0;
}
.image img {
width: 100%;
height: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-tile-image": HaTileImage;
}
}
+9 -15
View File
@@ -1,4 +1,4 @@
import { css, html, LitElement, svg } from "lit";
import { css, html, LitElement, nothing, svg } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { BRANCH_HEIGHT, SPACING } from "./hat-graph-const";
@@ -41,8 +41,8 @@ export class HatGraphBranch extends LitElement {
branches.push({
x: width / 2 + total_width,
height,
start: c.hasAttribute("graphStart"),
end: c.hasAttribute("graphEnd"),
start: c.hasAttribute("graph-start"),
end: c.hasAttribute("graph-end"),
track: c.hasAttribute("track"),
});
total_width += width;
@@ -65,11 +65,8 @@ export class HatGraphBranch extends LitElement {
return html`
<slot name="head"></slot>
${!this.start
? svg`
<svg
id="top"
width="${this._totalWidth}"
>
? html`
<svg id="top" width=${this._totalWidth}>
${this._branches.map((branch) =>
branch.start
? ""
@@ -86,7 +83,7 @@ export class HatGraphBranch extends LitElement {
)}
</svg>
`
: ""}
: nothing}
<div id="branches">
<svg id="lines" width=${this._totalWidth} height=${this._maxHeight}>
${this._branches.map((branch) => {
@@ -107,11 +104,8 @@ export class HatGraphBranch extends LitElement {
</div>
${!this.short
? svg`
<svg
id="bottom"
width="${this._totalWidth}"
>
? html`
<svg id="bottom" width=${this._totalWidth}>
${this._branches.map((branch) => {
if (branch.end) return "";
return svg`
@@ -128,7 +122,7 @@ export class HatGraphBranch extends LitElement {
})}
</svg>
`
: ""}
: nothing}
`;
}
+29
View File
@@ -108,6 +108,34 @@ interface PipelineIntentStartEvent extends PipelineEventBase {
intent_input: string;
};
}
interface ConversationChatLogAssistantDelta {
role: "assistant";
content: string;
tool_calls: {
id: string;
tool_name: string;
tool_args: Record<string, unknown>;
}[];
}
interface ConversationChatLogToolResultDelta {
role: "tool_result";
agent_id: string;
tool_call_id: string;
tool_name: string;
tool_result: unknown;
}
interface PipelineIntentProgressEvent extends PipelineEventBase {
type: "intent-progress";
data: {
chat_log_delta:
| Partial<ConversationChatLogAssistantDelta>
// These always come in 1 chunk
| ConversationChatLogToolResultDelta;
};
}
interface PipelineIntentEndEvent extends PipelineEventBase {
type: "intent-end";
data: {
@@ -141,6 +169,7 @@ export type PipelineRunEvent =
| PipelineSTTStartEvent
| PipelineSTTEndEvent
| PipelineIntentStartEvent
| PipelineIntentProgressEvent
| PipelineIntentEndEvent
| PipelineTTSStartEvent
| PipelineTTSEndEvent;
+56 -15
View File
@@ -1,3 +1,4 @@
import { memoize } from "@fullcalendar/core/internal";
import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
@@ -11,7 +12,9 @@ import type { HomeAssistant } from "../types";
import { fileDownload } from "../util/file_download";
import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation";
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
import checkValidDate from "../common/datetime/check_valid_date";
import { handleFetchPromise } from "../util/hass-call-api";
export const enum BackupScheduleRecurrence {
NEVER = "never",
@@ -104,6 +107,9 @@ export interface BackupContent {
name: string;
agents: Record<string, BackupContentAgent>;
failed_agent_ids?: string[];
extra_metadata?: {
"supervisor.addon_update"?: string;
};
with_automatic_settings: boolean;
}
@@ -125,7 +131,13 @@ export interface BackupContentExtended extends BackupContent, BackupData {}
export interface BackupInfo {
backups: BackupContent[];
backing_up: boolean;
agent_errors: Record<string, string>;
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
last_non_idle_event: ManagerStateEvent | null;
next_automatic_backup: string | null;
next_automatic_backup_additional: boolean;
state: BackupManagerState;
}
export interface BackupDetails {
@@ -227,27 +239,23 @@ export const restoreBackup = (
export const uploadBackup = async (
hass: HomeAssistant,
file: File,
agent_ids: string[]
): Promise<void> => {
agentIds: string[]
): Promise<{ backup_id: string }> => {
const fd = new FormData();
fd.append("file", file);
const params = agent_ids.reduce((acc, agent_id) => {
acc.append("agent_id", agent_id);
return acc;
}, new URLSearchParams());
const params = new URLSearchParams();
const resp = await hass.fetchWithAuth(
`/api/backup/upload?${params.toString()}`,
{
agentIds.forEach((agentId) => {
params.append("agent_id", agentId);
});
return handleFetchPromise(
hass.fetchWithAuth(`/api/backup/upload?${params.toString()}`, {
method: "POST",
body: fd,
}
})
);
if (!resp.ok) {
throw new Error(`${resp.status} ${resp.statusText}`);
}
};
export const getPreferredAgentForDownload = (agents: string[]) => {
@@ -319,6 +327,29 @@ export const computeBackupAgentName = (
export const computeBackupSize = (backup: BackupContent) =>
Math.max(...Object.values(backup.agents).map((agent) => agent.size));
export type BackupType = "automatic" | "manual" | "addon_update";
const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "manual", "addon_update"];
export const getBackupTypes = memoize((isHassio: boolean) =>
isHassio
? BACKUP_TYPE_ORDER
: BACKUP_TYPE_ORDER.filter((type) => type !== "addon_update")
);
export const computeBackupType = (
backup: BackupContent,
isHassio: boolean
): BackupType => {
if (backup.with_automatic_settings) {
return "automatic";
}
if (isHassio && backup.extra_metadata?.["supervisor.addon_update"] != null) {
return "addon_update";
}
return "manual";
};
export const compareAgents = (a: string, b: string) => {
const isLocalA = isLocalAgent(a);
const isLocalB = isLocalAgent(b);
@@ -422,3 +453,13 @@ export const getFormattedBackupTime = memoizeOne(
return `${formatTime(DEFAULT_OPTIMIZED_BACKUP_START_TIME, locale, config)} - ${formatTime(DEFAULT_OPTIMIZED_BACKUP_END_TIME, locale, config)}`;
}
);
export const SUPPORTED_UPLOAD_FORMAT = "application/x-tar";
export interface BackupUploadFileFormData {
file?: File;
}
export const INITIAL_UPLOAD_FORM_DATA: BackupUploadFileFormData = {
file: undefined,
};
+66
View File
@@ -0,0 +1,66 @@
import { handleFetchPromise } from "../util/hass-call-api";
import type { BackupContentExtended } from "./backup";
import type {
BackupManagerState,
RestoreBackupStage,
RestoreBackupState,
} from "./backup_manager";
export interface BackupOnboardingInfo {
state: BackupManagerState;
last_non_idle_event?: {
manager_state: BackupManagerState;
stage: RestoreBackupStage | null;
state: RestoreBackupState;
reason: string | null;
} | null;
}
export interface BackupOnboardingConfig extends BackupOnboardingInfo {
backups: BackupContentExtended[];
}
export const fetchBackupOnboardingInfo = async () =>
handleFetchPromise<BackupOnboardingConfig>(
fetch("/api/onboarding/backup/info")
);
export interface RestoreOnboardingBackupParams {
backup_id: string;
agent_id: string;
password?: string;
restore_addons?: string[];
restore_database?: boolean;
restore_folders?: string[];
}
export const restoreOnboardingBackup = async (
params: RestoreOnboardingBackupParams
) =>
handleFetchPromise(
fetch("/api/onboarding/backup/restore", {
method: "POST",
body: JSON.stringify(params),
})
);
export const uploadOnboardingBackup = async (
file: File,
agentIds: string[]
): Promise<{ backup_id: string }> => {
const fd = new FormData();
fd.append("file", file);
const params = new URLSearchParams();
agentIds.forEach((agentId) => {
params.append("agent_id", agentId);
});
return handleFetchPromise(
fetch(`/api/onboarding/backup/upload?${params.toString()}`, {
method: "POST",
body: fd,
})
);
};
+4
View File
@@ -73,6 +73,7 @@ export interface CloudWebhook {
interface CloudLoginBase {
hass: HomeAssistant;
email: string;
check_connection?: boolean;
}
export interface CloudLoginPassword extends CloudLoginBase {
@@ -181,3 +182,6 @@ export const updateCloudGoogleEntityConfig = (
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
hass.callApi("POST", "cloud/google_actions/sync");
export const fetchSupportPackage = (hass: HomeAssistant) =>
hass.callApi<string>("GET", "cloud/support_package");
+26
View File
@@ -19,6 +19,8 @@ export interface ConfigEntry {
supports_remove_device: boolean;
supports_unload: boolean;
supports_reconfigure: boolean;
supported_subentry_types: Record<string, { supports_reconfigure: boolean }>;
num_subentries: number;
pref_disable_new_entities: boolean;
pref_disable_polling: boolean;
disabled_by: "user" | null;
@@ -27,6 +29,30 @@ export interface ConfigEntry {
error_reason_translation_placeholders: Record<string, string> | null;
}
export interface SubEntry {
subentry_id: string;
subentry_type: string;
title: string;
unique_id: string;
}
export const getSubEntries = (hass: HomeAssistant, entry_id: string) =>
hass.callWS<SubEntry[]>({
type: "config_entries/subentries/list",
entry_id,
});
export const deleteSubEntry = (
hass: HomeAssistant,
entry_id: string,
subentry_id: string
) =>
hass.callWS({
type: "config_entries/subentries/delete",
entry_id,
subentry_id,
});
export type ConfigEntryMutableParams = Partial<
Pick<
ConfigEntry,
+5 -1
View File
@@ -2,7 +2,11 @@ import type { Connection } from "home-assistant-js-websocket";
import type { HaFormSchema } from "../components/ha-form/types";
import type { ConfigEntry } from "./config_entries";
export type FlowType = "config_flow" | "options_flow" | "repair_flow";
export type FlowType =
| "config_flow"
| "config_subentries_flow"
| "options_flow"
| "repair_flow";
export interface DataEntryFlowProgressedEvent {
type: "data_entry_flow_progressed";
+1
View File
@@ -17,6 +17,7 @@ export {
export interface DeviceRegistryEntry extends RegistryEntry {
id: string;
config_entries: string[];
config_entries_subentries: Record<string, (string | null)[]>;
connections: [string, string][];
identifiers: [string, string][];
manufacturer: string | null;
+1
View File
@@ -50,6 +50,7 @@ export interface EntityRegistryEntry extends RegistryEntry {
icon: string | null;
platform: string;
config_entry_id: string | null;
config_subentry_id: string | null;
device_id: string | null;
area_id: string | null;
labels: string[];
+21 -41
View File
@@ -1,6 +1,5 @@
import { atLeastVersion } from "../../common/config/version";
import type { HomeAssistant } from "../../types";
import { handleFetchPromise } from "../../util/hass-call-api";
import type { HassioResponse } from "./common";
import { hassioApiResultExtractor } from "./common";
@@ -82,34 +81,24 @@ export const fetchHassioBackups = async (
};
export const fetchHassioBackupInfo = async (
hass: HomeAssistant | undefined,
hass: HomeAssistant,
backup: string
): Promise<HassioBackupDetail> => {
if (hass) {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`,
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioBackupDetail>>(
"GET",
`hassio/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`
)
);
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`,
method: "get",
});
}
// When called from onboarding we don't have hass
return hassioApiResultExtractor(
await handleFetchPromise(
fetch(`/api/hassio/backups/${backup}/info`, {
method: "GET",
})
await hass.callApi<HassioResponse<HassioBackupDetail>>(
"GET",
`hassio/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`
)
);
};
@@ -240,24 +229,15 @@ export const uploadBackup = async (
};
export const restoreBackup = async (
hass: HomeAssistant | undefined,
hass: HomeAssistant,
type: HassioBackupDetail["type"],
backupSlug: string,
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
useSnapshotUrl: boolean
useBackupUrl: boolean
): Promise<void> => {
if (hass) {
await hass.callApi<HassioResponse<{ job_id: string }>>(
"POST",
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
backupDetails
);
} else {
await handleFetchPromise(
fetch(`/api/hassio/backups/${backupSlug}/restore/${type}`, {
method: "POST",
body: JSON.stringify(backupDetails),
})
);
}
await hass.callApi<HassioResponse<{ job_id: string }>>(
"POST",
`hassio/${useBackupUrl ? "backups" : "snapshots"}/${backupSlug}/restore/${type}`,
backupDetails
);
};
+33
View File
@@ -0,0 +1,33 @@
import type { HomeAssistant } from "../types";
import {
fetchFrontendUserData,
saveFrontendUserData,
subscribeFrontendUserData,
} from "./frontend";
export const SIDEBAR_PREFERENCES_KEY = "sidebar";
export interface SidebarPreferences {
panelOrder?: string[];
hiddenPanels?: string[];
}
declare global {
interface FrontendUserData {
sidebar?: SidebarPreferences;
}
}
export const fetchSidebarPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY);
export const saveSidebarPreferences = (
hass: HomeAssistant,
data: SidebarPreferences
) => saveFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY, data);
export const subscribeSidebarPreferences = (
hass: HomeAssistant,
callback: (sidebar?: SidebarPreferences | null) => void
) =>
subscribeFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY, callback);
+46
View File
@@ -0,0 +1,46 @@
import type { HomeAssistant } from "../types";
import type { DataEntryFlowStep } from "./data_entry_flow";
const HEADERS = {
"HA-Frontend-Base": `${location.protocol}//${location.host}`,
};
export const createSubConfigFlow = (
hass: HomeAssistant,
configEntryId: string,
subFlowType: string,
subentry_id?: string
) =>
hass.callApi<DataEntryFlowStep>(
"POST",
"config/config_entries/subentries/flow",
{
handler: [configEntryId, subFlowType],
show_advanced_options: Boolean(hass.userData?.showAdvanced),
subentry_id,
},
HEADERS
);
export const fetchSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi<DataEntryFlowStep>(
"GET",
`config/config_entries/subentries/flow/${flowId}`,
undefined,
HEADERS
);
export const handleSubConfigFlowStep = (
hass: HomeAssistant,
flowId: string,
data: Record<string, any>
) =>
hass.callApi<DataEntryFlowStep>(
"POST",
`config/config_entries/subentries/flow/${flowId}`,
data,
HEADERS
);
export const deleteSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `config/config_entries/subentries/flow/${flowId}`);
+1
View File
@@ -63,6 +63,7 @@ export type TranslationCategory =
| "entity_component"
| "exceptions"
| "config"
| "config_subentries"
| "config_panel"
| "options"
| "device_automation"
@@ -282,6 +282,8 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.navigateToResult=${this._params
.navigateToResult}
></step-flow-create-entry>
`}
`}
@@ -77,7 +77,7 @@ export class FlowPreviewGeneric extends LitElement {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
return;
}
try {
@@ -147,7 +147,7 @@ class FlowPreviewTemplate extends LitElement {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
return;
}
try {
@@ -16,7 +16,9 @@ export const loadConfigFlowDialog = loadDataEntryFlowDialog;
export const showConfigFlowDialog = (
element: HTMLElement,
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig">
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
entryId?: string;
}
): void =>
showFlowDialog(element, dialogParams, {
flowType: "config_flow",
@@ -148,8 +148,8 @@ export interface DataEntryFlowDialogParams {
}) => void;
flowConfig: FlowConfig;
showAdvanced?: boolean;
entryId?: string;
dialogParentElement?: HTMLElement;
navigateToResult?: boolean;
}
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");
@@ -0,0 +1,275 @@
import { html } from "lit";
import type { ConfigEntry } from "../../data/config_entries";
import { domainToName } from "../../data/integration";
import {
createSubConfigFlow,
deleteSubConfigFlow,
fetchSubConfigFlow,
handleSubConfigFlowStep,
} from "../../data/sub_config_flow";
import type { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
import {
loadDataEntryFlowDialog,
showFlowDialog,
} from "./show-dialog-data-entry-flow";
export const loadSubConfigFlowDialog = loadDataEntryFlowDialog;
export const showSubConfigFlowDialog = (
element: HTMLElement,
configEntry: ConfigEntry,
flowType: string,
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
subEntryId?: string;
}
): void =>
showFlowDialog(element, dialogParams, {
flowType: "config_subentries_flow",
showDevices: true,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createSubConfigFlow(hass, handler, flowType, dialogParams.subEntryId),
hass.loadFragmentTranslation("config"),
hass.loadBackendTranslation("config_subentries", configEntry.domain),
hass.loadBackendTranslation("selector", configEntry.domain),
// Used as fallback if no header defined for step
hass.loadBackendTranslation("title", configEntry.domain),
]);
return step;
},
fetchFlow: async (hass, flowId) => {
const step = await fetchSubConfigFlow(hass, flowId);
await hass.loadFragmentTranslation("config");
await hass.loadBackendTranslation(
"config_subentries",
configEntry.domain
);
await hass.loadBackendTranslation("selector", configEntry.domain);
return step;
},
handleFlowStep: handleSubConfigFlowStep,
deleteFlow: deleteSubConfigFlow,
renderAbortDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.abort.${step.reason}`,
step.description_placeholders
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: step.reason;
},
renderShowFormStepHeader(hass, step) {
return (
hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
step.description_placeholders
) || hass.localize(`component.${configEntry.domain}.title`)
);
},
renderShowFormStepDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
step.description_placeholders
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
},
renderShowFormStepFieldLabel(hass, step, field, options) {
if (field.type === "expandable") {
return hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data.${field.name}`
) || field.name
);
},
renderShowFormStepFieldHelper(hass, step, field, options) {
if (field.type === "expandable") {
return hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.description`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data_description.${field.name}`,
step.description_placeholders
);
return description
? html`<ha-markdown breaks .content=${description}></ha-markdown>`
: "";
},
renderShowFormStepFieldError(hass, step, error) {
return (
hass.localize(
`component.${step.translation_domain || step.translation_domain || configEntry.domain}.config_subentries.${flowType}.error.${error}`,
step.description_placeholders
) || error
);
},
renderShowFormStepFieldLocalizeValue(hass, _step, key) {
return hass.localize(`component.${configEntry.domain}.selector.${key}`);
},
renderShowFormStepSubmitButton(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.submit`
) ||
hass.localize(
`ui.panel.config.integrations.config_flow.${
step.last_step === false ? "next" : "submit"
}`
)
);
},
renderExternalStepHeader(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
) ||
hass.localize(
"ui.panel.config.integrations.config_flow.external_step.open_site"
)
);
},
renderExternalStepDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
step.description_placeholders
);
return html`
<p>
${hass.localize(
"ui.panel.config.integrations.config_flow.external_step.description"
)}
</p>
${description
? html`
<ha-markdown
allowsvg
breaks
.content=${description}
></ha-markdown>
`
: ""}
`;
},
renderCreateEntryDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.create_entry.${
step.description || "default"
}`,
step.description_placeholders
);
return html`
${description
? html`
<ha-markdown
allowsvg
breaks
.content=${description}
></ha-markdown>
`
: ""}
<p>
${hass.localize(
"ui.panel.config.integrations.config_flow.created_config",
{ name: step.title }
)}
</p>
`;
},
renderShowFormProgressHeader(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
) || hass.localize(`component.${configEntry.domain}.title`)
);
},
renderShowFormProgressDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.progress.${step.progress_action}`,
step.description_placeholders
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
},
renderMenuHeader(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
step.description_placeholders
) || hass.localize(`component.${configEntry.domain}.title`)
);
},
renderMenuDescription(hass, step) {
const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
step.description_placeholders
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
},
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.menu_options.${option}`,
step.description_placeholders
);
},
renderLoadingDescription(hass, reason, handler, step) {
if (reason !== "loading_flow" && reason !== "loading_step") {
return "";
}
const domain = step?.handler || handler;
return hass.localize(
`ui.panel.config.integrations.config_flow.loading.${reason}`,
{
integration: domain
? domainToName(hass.localize, domain)
: // when we are continuing a config flow, we only know the ID and not the domain
hass.localize(
"ui.panel.config.integrations.config_flow.loading.fallback_title"
),
}
);
},
});
@@ -60,6 +60,7 @@ class StepFlowAbort extends LitElement {
dialogClosedCallback: this.params.dialogClosedCallback,
startFlowHandler: this.domain,
showAdvanced: this.hass.userData?.showAdvanced,
navigateToResult: this.params.navigateToResult,
});
},
});
@@ -19,6 +19,7 @@ import { showAlertDialog } from "../generic/show-dialog-box";
import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog";
import type { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
import { navigate } from "../../common/navigate";
@customElement("step-flow-create-entry")
class StepFlowCreateEntry extends LitElement {
@@ -28,6 +29,8 @@ class StepFlowCreateEntry extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
navigateToResult = false;
private _devices = memoizeOne(
(
showDevices: boolean,
@@ -65,7 +68,8 @@ class StepFlowCreateEntry extends LitElement {
if (
devices.length !== 1 ||
devices[0].primary_config_entry !== this.step.result?.entry_id
devices[0].primary_config_entry !== this.step.result?.entry_id ||
this.step.result.domain === "voip"
) {
return;
}
@@ -81,6 +85,7 @@ class StepFlowCreateEntry extends LitElement {
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
)
) {
this.navigateToResult = false;
this._flowDone();
showVoiceAssistantSetupDialog(this, {
deviceId: devices[0].id,
@@ -151,6 +156,11 @@ class StepFlowCreateEntry extends LitElement {
private _flowDone(): void {
fireEvent(this, "flow-update", { step: undefined });
if (this.step.result && this.navigateToResult) {
navigate(
`/config/integrations/integration/${this.step.result.domain}#config_entry=${this.step.result.entry_id}`
);
}
}
private async _areaPicked(ev: CustomEvent) {
+7 -1
View File
@@ -40,8 +40,13 @@ export class DialogEnterCode
@state() private _showClearButton = false;
@state() private _narrow = false;
public async showDialog(dialogParams: EnterCodeDialogParams): Promise<void> {
this._dialogParams = dialogParams;
this._narrow = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
await this.updateComplete;
}
@@ -96,7 +101,7 @@ export class DialogEnterCode
>
<ha-textfield
class="input"
dialogInitialFocus
?dialogInitialFocus=${!this._narrow}
id="code"
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
type="password"
@@ -134,6 +139,7 @@ export class DialogEnterCode
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
type="password"
inputmode="numeric"
?dialogInitialFocus=${!this._narrow}
></ha-textfield>
<div class="keypad">
${BUTTONS.map((value) =>
@@ -49,6 +49,8 @@ class LightRgbColorPicker extends LitElement {
@state() private _hsPickerValue?: [number, number];
@state() private _isInteracting?: boolean;
protected render() {
if (!this.stateObj) {
return nothing;
@@ -211,7 +213,10 @@ class LightRgbColorPicker extends LitElement {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("entityId") && !changedProps.has("hass")) {
if (
this._isInteracting ||
(!changedProps.has("entityId") && !changedProps.has("hass"))
) {
return;
}
@@ -219,10 +224,13 @@ class LightRgbColorPicker extends LitElement {
}
private _hsColorCursorMoved(ev: CustomEvent) {
if (!ev.detail.value) {
const color = ev.detail.value;
this._isInteracting = color !== undefined;
if (color === undefined) {
return;
}
this._hsPickerValue = ev.detail.value;
this._hsPickerValue = color;
this._throttleUpdateColor();
}
@@ -22,7 +22,6 @@ import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity_attributes";
declare global {
interface HASSDomEvents {
"color-changed": LightColor;
"color-hovered": LightColor | undefined;
}
}
@@ -54,6 +53,8 @@ class LightColorTempPicker extends LitElement {
@state() private _ctPickerValue?: number;
@state() private _isInteracting?: boolean;
protected render() {
if (!this.stateObj) {
return nothing;
@@ -113,7 +114,7 @@ class LightColorTempPicker extends LitElement {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("stateObj")) {
if (this._isInteracting || !changedProps.has("stateObj")) {
return;
}
@@ -123,16 +124,14 @@ class LightColorTempPicker extends LitElement {
private _ctColorCursorMoved(ev: CustomEvent) {
const ct = ev.detail.value;
this._isInteracting = ct !== undefined;
if (isNaN(ct) || this._ctPickerValue === ct) {
return;
}
this._ctPickerValue = ct;
fireEvent(this, "color-hovered", {
color_temp_kelvin: ct,
});
this._throttleUpdateColorTemp();
}
@@ -143,8 +142,6 @@ class LightColorTempPicker extends LitElement {
private _ctColorChanged(ev: CustomEvent) {
const ct = ev.detail.value;
fireEvent(this, "color-hovered", undefined);
if (isNaN(ct) || this._ctPickerValue === ct) {
return;
}
@@ -99,7 +99,12 @@ class MoreInfoSirenAdvancedControls extends LitElement {
this._stateObj.attributes.available_tones
).map(
([toneId, toneName]) => html`
<ha-list-item .value=${toneId}
<ha-list-item
.value=${Array.isArray(
this._stateObj!.attributes.available_tones
)
? toneName
: toneId}
>${toneName}</ha-list-item
>
`
@@ -179,7 +184,7 @@ class MoreInfoSirenAdvancedControls extends LitElement {
await this.hass.callService("siren", "turn_on", {
entity_id: this._stateObj!.entity_id,
tone: this._tone,
volume: this._volume,
volume_level: this._volume,
duration: this._duration,
});
}
+1
View File
@@ -33,6 +33,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"switch",
"valve",
"water_heater",
"weather",
];
/** Domains with full height more info dialog */
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];
@@ -448,6 +448,10 @@ class MoreInfoUpdate extends LitElement {
box-sizing: border-box;
margin-bottom: -16px;
margin-top: -4px;
--md-sys-color-surface: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
}
ha-md-list-item {
@@ -1,18 +1,13 @@
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import {
mdiEye,
mdiGauge,
mdiThermometer,
mdiWaterPercent,
mdiWeatherWindy,
} from "@mdi/js";
import type { PropertyValues } from "lit";
import { mdiEye, mdiGauge, mdiWaterPercent, mdiWeatherWindy } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { formatDateWeekdayDay } from "../../../common/datetime/format_date";
import { formatTimeWeekday } from "../../../common/datetime/format_time";
import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
import { formatTime } from "../../../common/datetime/format_time";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-svg-icon";
import type {
ForecastEvent,
@@ -23,11 +18,16 @@ import {
getDefaultForecastType,
getForecast,
getSupportedForecastTypes,
getSecondaryWeatherAttribute,
getWeatherStateIcon,
getWeatherUnit,
getWind,
subscribeForecast,
weatherIcons,
weatherSVGStyles,
} from "../../../data/weather";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-relative-time";
import "../../../components/ha-state-icon";
@customElement("more-info-weather")
class MoreInfoWeather extends LitElement {
@@ -137,23 +137,90 @@ class MoreInfoWeather extends LitElement {
const hourly = forecastData?.type === "hourly";
const dayNight = forecastData?.type === "twice_daily";
const weatherStateIcon = getWeatherStateIcon(this.stateObj.state, this);
return html`
${this._showValue(this.stateObj.attributes.temperature)
? html`
<div class="flex">
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
<div class="main">
${this.hass.localize("ui.card.weather.attributes.temperature")}
</div>
<div>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"temperature"
)}
</div>
<div class="content">
<div class="icon-image">
${weatherStateIcon ||
html`
<ha-state-icon
class="weather-icon"
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-icon>
`}
</div>
<div class="info">
<div class="name-state">
<div class="state">
${this.hass.formatEntityState(this.stateObj)}
</div>
`
: ""}
<div class="time-ago">
<ha-relative-time
id="last_changed"
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
<simple-tooltip animation-delay="0" for="last_changed">
<div>
<div class="row">
<span class="column-name">
${this.hass.localize(
"ui.dialogs.more_info_control.last_changed"
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.dialogs.more_info_control.last_updated"
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_updated}
capitalize
></ha-relative-time>
</div>
</div>
</simple-tooltip>
</div>
</div>
<div class="temp-attribute">
<div class="temp">
${this.stateObj.attributes.temperature !== undefined &&
this.stateObj.attributes.temperature !== null
? html`
${formatNumber(
this.stateObj.attributes.temperature,
this.hass.locale
)}&nbsp;<span
>${getWeatherUnit(
this.hass.config,
this.stateObj,
"temperature"
)}</span
>
`
: nothing}
</div>
<div class="attribute">
${getSecondaryWeatherAttribute(
this.hass,
this.stateObj,
forecast!
)}
</div>
</div>
</div>
</div>
${this._showValue(this.stateObj.attributes.pressure)
? html`
<div class="flex">
@@ -169,7 +236,7 @@ class MoreInfoWeather extends LitElement {
</div>
</div>
`
: ""}
: nothing}
${this._showValue(this.stateObj.attributes.humidity)
? html`
<div class="flex">
@@ -185,7 +252,7 @@ class MoreInfoWeather extends LitElement {
</div>
</div>
`
: ""}
: nothing}
${this._showValue(this.stateObj.attributes.wind_speed)
? html`
<div class="flex">
@@ -203,7 +270,7 @@ class MoreInfoWeather extends LitElement {
</div>
</div>
`
: ""}
: nothing}
${this._showValue(this.stateObj.attributes.visibility)
? html`
<div class="flex">
@@ -219,7 +286,7 @@ class MoreInfoWeather extends LitElement {
</div>
</div>
`
: ""}
: nothing}
${forecast
? html`
<div class="section">
@@ -242,76 +309,90 @@ class MoreInfoWeather extends LitElement {
)}
</mwc-tab-bar>`
: nothing}
${forecast.map((item) =>
this._showValue(item.templow) || this._showValue(item.temperature)
? html`<div class="flex">
${item.condition
? html`
<ha-svg-icon
.path=${weatherIcons[item.condition]}
></ha-svg-icon>
`
: ""}
<div class="main">
${dayNight
? html`
${formatDateWeekdayDay(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
(${item.is_daytime !== false
? this.hass!.localize("ui.card.weather.day")
: this.hass!.localize("ui.card.weather.night")})
`
: hourly
<div class="forecast">
${forecast.map((item) =>
this._showValue(item.templow) ||
this._showValue(item.temperature)
? html`
<div>
<div>
${dayNight
? html`
${formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
<div class="daynight">
${item.is_daytime !== false
? this.hass!.localize("ui.card.weather.day")
: this.hass!.localize(
"ui.card.weather.night"
)}<br />
</div>
`
: hourly
? html`
${formatTime(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
`
: html`
${formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
`}
</div>
${this._showValue(item.condition)
? html`
${formatTimeWeekday(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
<div class="forecast-image-icon">
${getWeatherStateIcon(
item.condition!,
this,
!(
item.is_daytime ||
item.is_daytime === undefined
)
)}
</div>
`
: html`
${formatDateWeekdayDay(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
`}
</div>
<div class="templow">
${this._showValue(item.templow)
? this.hass.formatEntityAttributeValue(
this.stateObj!,
"templow",
item.templow
)
: hourly
? ""
: "—"}
</div>
<div class="temp">
${this._showValue(item.temperature)
? this.hass.formatEntityAttributeValue(
this.stateObj!,
"temperature",
item.temperature
)
: "—"}
</div>
</div>`
: ""
)}
: nothing}
<div class="temp">
${this._showValue(item.temperature)
? html`${formatNumber(
item.temperature,
this.hass!.locale
)}°`
: "—"}
</div>
<div class="templow">
${this._showValue(item.templow)
? html`${formatNumber(
item.templow!,
this.hass!.locale
)}°`
: hourly
? nothing
: "—"}
</div>
</div>
`
: nothing
)}
</div>
`
: ""}
: nothing}
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
${this.stateObj.attributes.attribution}
</div>
`
: ""}
: nothing}
`;
}
@@ -321,56 +402,186 @@ class MoreInfoWeather extends LitElement {
];
}
static styles = css`
ha-svg-icon {
color: var(--paper-item-icon-color);
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
static get styles(): CSSResultGroup {
return [
weatherSVGStyles,
css`
ha-svg-icon {
color: var(--paper-item-icon-color);
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
mwc-tab-bar {
margin-bottom: 4px;
}
mwc-tab-bar {
margin-bottom: 4px;
}
.section {
margin: 16px 0 8px 0;
font-size: 1.2em;
}
.section {
margin: 16px 0 8px 0;
font-size: 1.2em;
}
.flex {
display: flex;
height: 32px;
align-items: center;
}
.flex > div:last-child {
direction: ltr;
}
.flex {
display: flex;
height: 32px;
align-items: center;
}
.flex > div:last-child {
direction: ltr;
}
.main {
flex: 1;
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
}
.main {
flex: 1;
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
}
.temp,
.templow {
min-width: 48px;
text-align: right;
direction: ltr;
}
.attribution {
text-align: center;
margin-top: 16px;
}
.templow {
margin: 0 16px;
color: var(--secondary-text-color);
}
.time-ago,
.attribute {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.attribution {
color: var(--secondary-text-color);
text-align: center;
}
`;
.attribution,
.templow,
.daynight,
.attribute,
.time-ago {
color: var(--secondary-text-color);
}
.content {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.icon-image {
display: flex;
align-items: center;
min-width: 64px;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
.icon-image > * {
flex: 0 0 64px;
height: 64px;
}
.weather-icon {
--mdc-icon-size: 64px;
}
.info {
display: flex;
justify-content: space-between;
flex-grow: 1;
overflow: hidden;
}
.temp-attribute {
text-align: var(--float-end);
}
.temp-attribute .temp {
position: relative;
margin-right: 24px;
direction: ltr;
}
.temp-attribute .temp span {
position: absolute;
font-size: 24px;
top: 1px;
}
.state,
.temp-attribute .temp {
font-size: 28px;
line-height: 1.2;
}
.attribute {
font-size: 14px;
line-height: 1;
}
.name-state {
overflow: hidden;
padding-right: 12px;
padding-inline-end: 12px;
padding-inline-start: initial;
width: 100%;
}
.state {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.forecast {
display: flex;
justify-content: space-around;
padding: 16px;
padding-bottom: 0px;
overflow-x: auto;
scrollbar-color: var(--scrollbar-thumb-color) transparent;
scrollbar-width: thin;
mask-image: linear-gradient(
90deg,
transparent 0%,
black 5%,
black 94%,
transparent 100%
);
}
.forecast > div {
text-align: center;
padding: 0 10px;
}
.forecast .icon,
.forecast .temp {
margin: 4px 0;
}
.forecast .temp {
font-size: 16px;
}
.forecast-image-icon {
padding-top: 4px;
padding-bottom: 4px;
display: flex;
justify-content: center;
}
.forecast-image-icon > * {
width: 40px;
height: 40px;
--mdc-icon-size: 40px;
}
.forecast-icon {
--mdc-icon-size: 40px;
}
`,
];
}
private _showValue(item: number | string | undefined): boolean {
return typeof item !== "undefined" && item !== null;
+4
View File
@@ -251,6 +251,7 @@ export class QuickBar extends LitElement {
<mwc-list>
${this._opened
? html`<lit-virtualizer
tabindex="-1"
scroller
@keydown=${this._handleListItemKeyDown}
@rangechange=${this._handleRangeChanged}
@@ -326,6 +327,7 @@ export class QuickBar extends LitElement {
.twoline=${Boolean(item.area)}
.item=${item}
index=${ifDefined(index)}
tabindex="0"
>
<span>${item.primaryText}</span>
${item.area
@@ -346,6 +348,7 @@ export class QuickBar extends LitElement {
.item=${item}
index=${ifDefined(index)}
graphic="icon"
tabindex="0"
>
${item.iconPath
? html`
@@ -375,6 +378,7 @@ export class QuickBar extends LitElement {
index=${ifDefined(index)}
class="command-item"
hasMeta
tabindex="0"
>
<span>
<ha-label
@@ -10,6 +10,7 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { cloudLogin } from "../../../data/cloud";
import { showCloudAlreadyConnectedDialog } from "../../../panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected";
import type { HomeAssistant } from "../../../types";
import {
showAlertDialog,
@@ -25,6 +26,8 @@ export class CloudStepSignin extends LitElement {
@state() private _error?: string;
@state() private _checkConnection = true;
@query("#email", true) private _emailField!: HaTextField;
@query("#password", true) private _passwordField!: HaPasswordField;
@@ -115,6 +118,7 @@ export class CloudStepSignin extends LitElement {
hass: this.hass,
email: username,
...(code ? { code } : { password }),
check_connection: this._checkConnection,
});
} catch (err: any) {
const errCode = err && err.body && err.body.code;
@@ -139,6 +143,20 @@ export class CloudStepSignin extends LitElement {
}
}
if (errCode === "alreadyconnectederror") {
showCloudAlreadyConnectedDialog(this, {
details: JSON.parse(err.body.message),
logInHereAction: () => {
this._checkConnection = false;
doLogin(username);
},
closeDialog: () => {
this._requestInProgress = false;
},
});
return;
}
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
await doLogin(username.toLowerCase());
return;
@@ -47,6 +47,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
@state() private _assistConfiguration?: AssistSatelliteConfiguration;
@state() private _error?: string;
private _previousSteps: STEP[] = [];
private _nextStep?: STEP;
@@ -165,79 +167,86 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
"update"
)}
></ha-voice-assistant-setup-step-update>`
: assistEntityState?.state === UNAVAILABLE
? this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.not_available"
)
: this._step === STEP.CHECK
? html`<ha-voice-assistant-setup-step-check
.hass=${this.hass}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-check>`
: this._step === STEP.WAKEWORD
? html`<ha-voice-assistant-setup-step-wake-word
: this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: assistEntityState?.state === UNAVAILABLE
? html`<ha-alert alert-type="error"
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.not_available"
)}</ha-alert
>`
: this._step === STEP.CHECK
? html`<ha-voice-assistant-setup-step-check
.hass=${this.hass}
.assistConfiguration=${this._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
.deviceEntities=${this._deviceEntities(
this._params.deviceId,
this.hass.entities
)}
></ha-voice-assistant-setup-step-wake-word>`
: this._step === STEP.CHANGE_WAKEWORD
? html`
<ha-voice-assistant-setup-step-change-wake-word
.hass=${this.hass}
.assistConfiguration=${this._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-change-wake-word>
`
: this._step === STEP.AREA
></ha-voice-assistant-setup-step-check>`
: this._step === STEP.WAKEWORD
? html`<ha-voice-assistant-setup-step-wake-word
.hass=${this.hass}
.assistConfiguration=${this._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
.deviceEntities=${this._deviceEntities(
this._params.deviceId,
this.hass.entities
)}
></ha-voice-assistant-setup-step-wake-word>`
: this._step === STEP.CHANGE_WAKEWORD
? html`
<ha-voice-assistant-setup-step-area
.hass=${this.hass}
.deviceId=${this._params.deviceId}
></ha-voice-assistant-setup-step-area>
`
: this._step === STEP.PIPELINE
? html`<ha-voice-assistant-setup-step-pipeline
<ha-voice-assistant-setup-step-change-wake-word
.hass=${this.hass}
.assistConfiguration=${this._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-pipeline>`
: this._step === STEP.CLOUD
? html`<ha-voice-assistant-setup-step-cloud
></ha-voice-assistant-setup-step-change-wake-word>
`
: this._step === STEP.AREA
? html`
<ha-voice-assistant-setup-step-area
.hass=${this.hass}
></ha-voice-assistant-setup-step-cloud>`
: this._step === STEP.LOCAL
? html`<ha-voice-assistant-setup-step-local
.deviceId=${this._params.deviceId}
></ha-voice-assistant-setup-step-area>
`
: this._step === STEP.PIPELINE
? html`<ha-voice-assistant-setup-step-pipeline
.hass=${this.hass}
.assistConfiguration=${this._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-pipeline>`
: this._step === STEP.CLOUD
? html`<ha-voice-assistant-setup-step-cloud
.hass=${this.hass}
.assistConfiguration=${this
._assistConfiguration}
></ha-voice-assistant-setup-step-local>`
: this._step === STEP.SUCCESS
? html`<ha-voice-assistant-setup-step-success
></ha-voice-assistant-setup-step-cloud>`
: this._step === STEP.LOCAL
? html`<ha-voice-assistant-setup-step-local
.hass=${this.hass}
.assistConfiguration=${this
._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-success>`
: nothing}
></ha-voice-assistant-setup-step-local>`
: this._step === STEP.SUCCESS
? html`<ha-voice-assistant-setup-step-success
.hass=${this.hass}
.assistConfiguration=${this
._assistConfiguration}
.assistEntityId=${assistSatelliteEntityId}
></ha-voice-assistant-setup-step-success>`
: nothing}
</div>
</ha-dialog>
`;
}
private async _fetchAssistConfiguration() {
this._assistConfiguration = await fetchAssistSatelliteConfiguration(
this.hass,
this._findDomainEntityId(
this._params!.deviceId,
this.hass.entities,
"assist_satellite"
)!
);
return this._assistConfiguration;
try {
this._assistConfiguration = await fetchAssistSatelliteConfiguration(
this.hass,
this._findDomainEntityId(
this._params!.deviceId,
this.hass.entities,
"assist_satellite"
)!
);
} catch (err: any) {
this._error = err.message;
}
}
private _goToPreviousStep() {
@@ -293,6 +302,10 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
.skip-btn {
margin-top: 6px;
}
ha-alert {
margin: 24px;
display: block;
}
`,
];
}
@@ -85,7 +85,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
<div class="rows">
${this.assistConfiguration &&
this.assistConfiguration.available_wake_words.length > 1
? html` <div class="row">
? html`<div class="row">
<ha-select
.label=${"Wake word"}
@closed=${stopPropagation}
@@ -44,6 +44,15 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
protected override willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (changedProperties.has("assistConfiguration")) {
if (
this.assistConfiguration &&
!this.assistConfiguration.available_wake_words.length
) {
this._nextStep();
}
}
if (changedProperties.has("assistEntityId")) {
this._detected = false;
this._muteSwitchEntity = this.deviceEntities?.find(
@@ -135,13 +144,16 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
>`
: nothing}
</div>
<div class="footer centered">
<ha-button @click=${this._changeWakeWord}
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.wake_word.change_wake_word"
)}</ha-button
>
</div>`;
${this.assistConfiguration &&
this.assistConfiguration.available_wake_words.length > 1
? html`<div class="footer centered">
<ha-button @click=${this._changeWakeWord}
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.wake_word.change_wake_word"
)}</ha-button
>
</div>`
: nothing}`;
}
private async _listenWakeWord() {
+1 -1
View File
@@ -17,7 +17,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
@property({ attribute: false }) public localize: LocalizeFunc = empty;
// Use browser language setup before login.
@property() public language?: string = getLocalLanguage();
@property() public language: string = getLocalLanguage();
@property() public translationFragment?: string;
+5 -9
View File
@@ -41,6 +41,7 @@ import "./onboarding-analytics";
import "./onboarding-create-user";
import "./onboarding-loading";
import "./onboarding-welcome";
import "./onboarding-restore-backup";
import "./onboarding-welcome-links";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { navigate } from "../common/navigate";
@@ -157,8 +158,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
private _renderStep() {
if (this._restoring) {
return html`<onboarding-restore-backup
.hass=${this.hass}
.localize=${this.localize}
.supervisor=${this._supervisor ?? false}
.language=${this.language}
>
</onboarding-restore-backup>`;
}
@@ -166,8 +168,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
if (this._init) {
return html`<onboarding-welcome
.localize=${this.localize}
.language=${this.language}
.supervisor=${this._supervisor}
></onboarding-welcome>`;
}
@@ -236,7 +236,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
}
}
if (changedProps.has("language")) {
document.querySelector("html")!.setAttribute("lang", this.language!);
document.querySelector("html")!.setAttribute("lang", this.language);
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
@@ -272,10 +272,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
"Home Assistant OS",
"Home Assistant Supervised",
].includes(response.installation_type);
if (this._supervisor) {
// Only load if we have supervisor
import("./onboarding-restore-backup");
}
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(
@@ -454,7 +450,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
subscribeOne(conn, subscribeUser),
]);
this.initializeHass(auth, conn);
if (this.language && this.language !== this.hass!.language) {
if (this.language !== this.hass!.language) {
this._updateHass({
locale: { ...this.hass!.locale, language: this.language },
language: this.language,
+294 -94
View File
@@ -1,136 +1,336 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
import "../../hassio/src/components/hassio-upload-backup";
import "./restore-backup/onboarding-restore-backup-upload";
import "./restore-backup/onboarding-restore-backup-details";
import "./restore-backup/onboarding-restore-backup-restore";
import "./restore-backup/onboarding-restore-backup-status";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-ansi-to-html";
import "../components/ha-card";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-circular-progress";
import "../components/ha-alert";
import "../components/ha-button";
import { fetchInstallationType } from "../data/onboarding";
import type { HomeAssistant } from "../types";
import "./onboarding-loading";
import { onBoardingStyles } from "./styles";
import { removeSearchParam } from "../common/url/search-params";
import { navigate } from "../common/navigate";
import { onBoardingStyles } from "./styles";
import {
fetchBackupOnboardingInfo,
type BackupOnboardingConfig,
type BackupOnboardingInfo,
} from "../data/backup_onboarding";
import type { BackupContentExtended, BackupData } from "../data/backup";
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
import { storage } from "../common/decorators/storage";
const STATUS_INTERVAL_IN_MS = 5000;
@customElement("onboarding-restore-backup")
class OnboardingRestoreBackup extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property() public language!: string;
@state() private _restoring = false;
@property({ type: Boolean }) public supervisor = false;
@state() private _backupSlug?: string;
@state() private _view:
| "loading"
| "upload"
| "select_data"
| "confirm_restore"
| "status" = "loading";
@state() private _backup?: BackupContentExtended;
@state() private _backupInfo?: BackupOnboardingInfo;
@state() private _selectedData?: BackupData;
@state() private _error?: string;
@state() private _failed?: boolean;
@storage({
key: "onboarding-restore-backup-backup-id",
})
private _backupId?: string;
@storage({
key: "onboarding-restore-running",
})
private _restoreRunning?: boolean;
protected render(): TemplateResult {
return html`
${this._restoring
? html`<h1>
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
</h1>
<ha-alert alert-type="info">
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
</ha-alert>
<onboarding-loading></onboarding-loading>`
: html` <h1>
${this.localize("ui.panel.page-onboarding.restore.header")}
</h1>
<hassio-upload-backup
@backup-uploaded=${this._backupUploaded}
@backup-cleared=${this._backupCleared}
.hass=${this.hass}
.localize=${this.localize}
></hassio-upload-backup>`}
<div class="footer">
<ha-button @click=${this._back} .disabled=${this._restoring}>
${this.localize("ui.panel.page-onboarding.back")}
</ha-button>
${this._backupSlug
? html`<ha-button
@click=${this._showBackupDialog}
.disabled=${this._restoring}
${
this._view !== "status" || this._failed
? html`<ha-icon-button-arrow-prev
.label=${this.localize("ui.panel.page-onboarding.restore.back")}
@click=${this._back}
></ha-icon-button-arrow-prev>`
: nothing
}
</ha-icon-button>
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
${
this._error || (this._failed && this._view !== "status")
? html`<ha-alert
alert-type="error"
.title=${this._failed && this._view !== "status"
? this.localize("ui.panel.page-onboarding.restore.failed")
: ""}
>
${this.localize("ui.panel.page-onboarding.restore.restore")}
</ha-button>`
: nothing}
</div>
${this._failed && this._view !== "status"
? this.localize(
`ui.panel.page-onboarding.restore.${this._backupInfo?.last_non_idle_event?.reason === "password_incorrect" ? "failed_wrong_password_description" : "failed_description"}`
)
: this._error}
</ha-alert>`
: nothing
}
${
this._view === "loading"
? html`<div class="loading">
<ha-circular-progress indeterminate></ha-circular-progress>
</div>`
: this._view === "upload"
? html`
<onboarding-restore-backup-upload
.supervisor=${this.supervisor}
.localize=${this.localize}
@backup-uploaded=${this._backupUploaded}
></onboarding-restore-backup-upload>
`
: this._view === "select_data"
? html`<onboarding-restore-backup-details
.localize=${this.localize}
.backup=${this._backup!}
@backup-restore=${this._restore}
></onboarding-restore-backup-details>`
: this._view === "confirm_restore"
? html`<onboarding-restore-backup-restore
.localize=${this.localize}
.backup=${this._backup!}
.supervisor=${this.supervisor}
.selectedData=${this._selectedData!}
@restore-started=${this._restoreStarted}
></onboarding-restore-backup-restore>`
: nothing
}
${
this._view === "status" && this._backupInfo
? html`<onboarding-restore-backup-status
.localize=${this.localize}
.backupInfo=${this._backupInfo}
@show-backup-upload=${this._reupload}
></onboarding-restore-backup-status>`
: nothing
}
${
["select_data", "confirm_restore"].includes(this._view) && this._backup
? html`<div class="backup-summary-wrapper">
<ha-backup-details-summary
translation-key-panel="page-onboarding.restore"
show-upload-another
.backup=${this._backup}
.localize=${this.localize}
@show-backup-upload=${this._reupload}
.isHassio=${this.supervisor}
></ha-backup-details-summary>
</div>`
: nothing
}
`;
}
private _back(): void {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
}
private _backupUploaded(ev) {
const backup = ev.detail.backup;
this._backupSlug = backup.slug;
this._showBackupDialog();
}
private _backupCleared() {
this._backupSlug = undefined;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._loadBackupInfo();
}
private async _checkRestoreStatus(): Promise<void> {
if (this._restoring) {
try {
await fetchInstallationType();
} catch (err: any) {
private async _loadBackupInfo() {
let onboardingInfo: BackupOnboardingConfig;
try {
onboardingInfo = await fetchBackupOnboardingInfo();
} catch (err: any) {
if (this._restoreRunning) {
if (
(err as Error).message === "unauthorized" ||
(err as Error).message === "not_found"
err.error === "Request error" ||
// core can restart but haven't loaded the backup integration yet
(err.status_code === 500 && err.body?.error === "backup_disabled")
) {
// core is down because of restore, keep trying
this._scheduleLoadBackupInfo();
return;
}
// core seems to be back up restored
if (err.status_code === 404) {
this._restoreRunning = undefined;
this._backupId = undefined;
window.location.replace("/");
return;
}
}
this._error = err?.message || "Cannot get backup info";
// if we are in an unknown state, show upload
if (this._view === "loading") {
this._view = "upload";
}
return;
}
const {
last_non_idle_event: lastNonIdleEvent,
state: currentState,
backups,
} = onboardingInfo;
this._backupInfo = {
state: currentState,
last_non_idle_event: lastNonIdleEvent,
};
if (this._backupId) {
this._backup = backups.find(
({ backup_id }) => backup_id === this._backupId
);
}
const failedRestore =
lastNonIdleEvent?.manager_state === "restore_backup" &&
lastNonIdleEvent?.state === "failed";
if (failedRestore) {
this._failed = true;
}
if (this._restoreRunning) {
this._view = "status";
if (failedRestore || currentState !== "restore_backup") {
this._failed = true;
this._restoreRunning = undefined;
} else {
this._scheduleLoadBackupInfo();
}
return;
}
if (
this._backup &&
// after backup was uploaded
(lastNonIdleEvent?.manager_state === "receive_backup" ||
// when restore was confirmed but failed to start (for example, encryption key was wrong)
failedRestore)
) {
if (!this.supervisor && this._backup.homeassistant_included) {
this._selectedData = {
homeassistant_included: true,
folders: [],
addons: [],
homeassistant_version: this._backup.homeassistant_version,
database_included: this._backup.database_included,
};
// skip select data when supervisor is not available and backup includes HA
this._view = "confirm_restore";
} else {
this._view = "select_data";
}
return;
}
// show upload as default
this._view = "upload";
}
private _scheduleLoadBackupInfo() {
setTimeout(() => this._loadBackupInfo(), STATUS_INTERVAL_IN_MS);
}
private async _backupUploaded(ev: CustomEvent) {
this._backupId = ev.detail.backupId;
await this._loadBackupInfo();
}
private async _restoreStarted() {
if (this._backupInfo) {
this._backupInfo.state = "restore_backup";
}
this._view = "status";
this._restoreRunning = true;
await this._loadBackupInfo();
}
private async _back() {
if (this._view === "upload" || (this._view === "status" && this._failed)) {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
} else {
const confirmed = await showConfirmationDialog(this, {
title: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.title"
),
text: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.text"
),
confirmText: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.yes"
),
dismissText: this.localize(
"ui.panel.page-onboarding.restore.cancel_restore.no"
),
});
if (!confirmed) {
return;
}
navigate(`${location.pathname}?${removeSearchParam("page")}`);
}
}
private _scheduleCheckRestoreStatus(): void {
setTimeout(() => this._checkRestoreStatus(), 1000);
private _restore(ev: CustomEvent) {
if (!this._backup || !ev.detail.selectedData) {
return;
}
this._selectedData = ev.detail.selectedData;
this._view = "confirm_restore";
}
private _showBackupDialog(): void {
showHassioBackupDialog(this, {
slug: this._backupSlug!,
onboarding: true,
localize: this.localize,
onRestoring: () => {
this._restoring = true;
this._scheduleCheckRestoreStatus();
},
});
private _reupload() {
this._backup = undefined;
this._backupId = undefined;
this._view = "upload";
}
static get styles(): CSSResultGroup {
return [
onBoardingStyles,
css`
:host {
display: flex;
flex-direction: column;
align-items: center;
}
hassio-upload-backup {
width: 100%;
}
.footer {
display: flex;
justify-content: space-between;
width: 100%;
}
`,
];
}
static styles = [
onBoardingStyles,
css`
:host {
display: flex;
flex-direction: column;
position: relative;
}
ha-icon-button-arrow-prev {
position: absolute;
top: 12px;
}
ha-card {
width: 100%;
}
.loading {
display: flex;
justify-content: center;
padding: 32px;
}
.backup-summary-wrapper {
margin-top: 24px;
padding: 0 20px;
}
`,
];
}
declare global {
+4 -8
View File
@@ -1,5 +1,5 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
@@ -13,8 +13,6 @@ class OnboardingWelcome extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ type: Boolean }) public supervisor = false;
protected render(): TemplateResult {
return html`
<h1>${this.localize("ui.panel.page-onboarding.welcome.header")}</h1>
@@ -24,11 +22,9 @@ class OnboardingWelcome extends LitElement {
${this.localize("ui.panel.page-onboarding.welcome.start")}
</ha-button>
${this.supervisor
? html`<ha-button @click=${this._restoreBackup}>
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
</ha-button>`
: nothing}
<ha-button @click=${this._restoreBackup}>
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
</ha-button>
`;
}
@@ -0,0 +1,57 @@
import { css, html, LitElement, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import "../../components/ha-card";
import "../../components/ha-circular-progress";
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../panels/config/backup/components/ha-backup-details-restore";
import "../../panels/config/backup/components/ha-backup-details-summary";
import { haStyle } from "../../resources/styles";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { BackupContentExtended } from "../../data/backup";
@customElement("onboarding-restore-backup-details")
class OnboardingRestoreBackupDetails extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: false }) public backup!: BackupContentExtended;
render() {
return html`
${this.backup.homeassistant_included
? html`<ha-backup-details-restore
.backup=${this.backup}
.localize=${this.localize}
translation-key-panel="page-onboarding.restore"
ha-required
></ha-backup-details-restore>`
: html`
<ha-alert alert-type="error">
${this.localize(
"ui.panel.page-onboarding.restore.details.home_assistant_missing"
)}
</ha-alert>
`}
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
padding: 28px 20px 0;
}
ha-backup-details-restore {
display: block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-restore-backup-details": OnboardingRestoreBackupDetails;
}
}
@@ -0,0 +1,175 @@
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-card";
import "../../components/ha-alert";
import "../../components/buttons/ha-progress-button";
import "../../components/ha-password-field";
import { haStyle } from "../../resources/styles";
import type { LocalizeFunc } from "../../common/translations/localize";
import {
CORE_LOCAL_AGENT,
HASSIO_LOCAL_AGENT,
type BackupContentExtended,
type BackupData,
} from "../../data/backup";
import { restoreOnboardingBackup } from "../../data/backup_onboarding";
import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("onboarding-restore-backup-restore")
class OnboardingRestoreBackupRestore extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: false }) public backup!: BackupContentExtended;
@property({ attribute: false })
public selectedData!: BackupData;
@property({ type: Boolean }) public supervisor = false;
@state() private _encryptionKey = "";
@state() private _encryptionKeyWrong = false;
@state() private _error?: string;
@state() private _loading = false;
render() {
const agentId = this.supervisor ? HASSIO_LOCAL_AGENT : CORE_LOCAL_AGENT;
const backupProtected = this.backup.agents[agentId].protected;
return html`
${this.backup.homeassistant_included &&
!this.supervisor &&
(this.backup.addons.length > 0 || this.backup.folders.length > 0)
? html`<ha-alert alert-type="warning" class="supervisor-warning">
${this.localize(
"ui.panel.page-onboarding.restore.details.addons_unsupported"
)}
</ha-alert>`
: nothing}
<ha-card
.header=${this.localize("ui.panel.page-onboarding.restore.restore")}
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
: nothing}
<p>
${this.localize(
"ui.panel.page-onboarding.restore.confirm_restore_full_backup_text"
)}
</p>
${backupProtected
? html`<p>
${this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.title"
)}
</p>
${this._encryptionKeyWrong
? html`
<ha-alert alert-type="error">
${this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key"
)}
</ha-alert>
`
: nothing}
<ha-password-field
.disabled=${this._loading}
@input=${this._encryptionKeyChanged}
.label=${this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.input_label"
)}
.value=${this._encryptionKey}
></ha-password-field>`
: nothing}
</div>
<div class="card-actions">
<ha-progress-button
.progress=${this._loading}
.disabled=${this._loading ||
(backupProtected && this._encryptionKey === "")}
@click=${this._startRestore}
destructive
>
${this.localize(
"ui.panel.page-onboarding.restore.details.restore.action"
)}
</ha-progress-button>
</div>
</ha-card>
`;
}
private _encryptionKeyChanged(ev): void {
this._encryptionKey = ev.target.value;
}
private async _startRestore(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as HaProgressButton;
this._loading = true;
this._error = undefined;
this._encryptionKeyWrong = false;
const backupAgent = this.supervisor ? HASSIO_LOCAL_AGENT : CORE_LOCAL_AGENT;
try {
await restoreOnboardingBackup({
agent_id: backupAgent,
backup_id: this.backup.backup_id,
password: this._encryptionKey || undefined,
restore_addons: this.selectedData.addons.map((addon) => addon.slug),
restore_database: this.selectedData.database_included,
restore_folders: this.selectedData.folders,
});
button.actionSuccess();
fireEvent(this, "restore-started");
} catch (err: any) {
if (err.error === "Request error") {
// core can shutdown before we get a response
button.actionSuccess();
fireEvent(this, "restore-started");
return;
}
button.actionError();
if (err.body?.code === "incorrect_password") {
this._encryptionKeyWrong = true;
} else {
this._error =
err.body?.message || err.message || "Unknown error occurred";
}
this._loading = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
padding: 28px 20px 0;
}
.card-actions {
display: flex;
justify-content: flex-end;
}
.supervisor-warning {
display: block;
margin-bottom: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-restore-backup-restore": OnboardingRestoreBackupRestore;
}
interface HASSDomEvents {
"restore-started";
}
}
@@ -0,0 +1,119 @@
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import "../../components/ha-card";
import "../../components/ha-circular-progress";
import "../../components/ha-alert";
import "../../components/ha-button";
import { haStyle } from "../../resources/styles";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { BackupOnboardingInfo } from "../../data/backup_onboarding";
import { fireEvent } from "../../common/dom/fire_event";
import { navigate } from "../../common/navigate";
import { removeSearchParam } from "../../common/url/search-params";
@customElement("onboarding-restore-backup-status")
class OnboardingRestoreBackupStatus extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: false })
public backupInfo!: BackupOnboardingInfo;
render() {
return html`
<ha-card
.header=${this.localize(
`ui.panel.page-onboarding.restore.${this.backupInfo.state === "restore_backup" ? "in_progress" : "failed"}`
)}
>
<div class="card-content">
${this.backupInfo.state === "restore_backup"
? html`
<div class="loading">
<ha-circular-progress indeterminate></ha-circular-progress>
</div>
<p>
${this.localize(
"ui.panel.page-onboarding.restore.in_progress_description"
)}
</p>
`
: html`
<ha-alert alert-type="error">
${this.localize(
"ui.panel.page-onboarding.restore.failed_status_description"
)}
</ha-alert>
${this.backupInfo.last_non_idle_event?.reason
? html`
<div class="failed">
<h4>Error:</h4>
${this.backupInfo.last_non_idle_event?.reason}
</div>
`
: nothing}
`}
</div>
${this.backupInfo.state !== "restore_backup"
? html`<div class="card-actions">
<ha-button @click=${this._uploadAnother} destructive>
${this.localize(
`ui.panel.page-onboarding.restore.details.summary.upload_another`
)}
</ha-button>
<ha-button @click=${this._home} destructive>
${this.localize(
`ui.panel.page-onboarding.restore.details.summary.home`
)}
</ha-button>
</div>`
: nothing}
</ha-card>
`;
}
private _uploadAnother() {
fireEvent(this, "show-backup-upload");
}
private _home() {
navigate(`${location.pathname}?${removeSearchParam("page")}`);
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
padding: 28px 20px 0;
}
.card-actions {
display: flex;
justify-content: flex-end;
}
.loading {
display: flex;
justify-content: center;
padding: 32px;
}
p {
text-align: center;
padding: 0 16px;
font-size: 16px;
}
.failed {
padding: 16px 0;
font-size: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-restore-backup-status": OnboardingRestoreBackupStatus;
}
interface HASSDomEvents {
"restore-started";
}
}

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