Compare commits

..

423 Commits

Author SHA1 Message Date
Paul Bottein
99cd67c857 Add entity filter section strategy 2025-03-14 12:20:48 +01:00
karwosts
91e9836423 Fix accessibility in add helper dialog (#24627) 2025-03-14 11:15:59 +01:00
Wendelin
6aa2a576b3 Revert "Unified safe area (insets) for Android and iOS" (#24629)
Revert "Unified safe area (insets) for Android and iOS (#23811)"

This reverts commit ee10f9080d.
2025-03-14 11:03:50 +01:00
Norbert Rittel
9712f04662 Fix short_weekdays::sun in Backup settings (#24628)
Fix short `short_weekdays::sun`

"So" is for us Germans, for English we better use "Su" here. :-)
2025-03-14 10:01:31 +01:00
renovate[bot]
731a9a2e07 Update dependency typescript-eslint to v8.26.1 (#24625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 09:50:34 +01:00
Simon Lamon
d42bd36a3e Fix logbook keeps loading (#24351)
* Logbook loading

* Redo typing

* small fixes

* async/sync fixes
2025-03-13 17:18:24 +01:00
Wendelin
28c355812c Add shoelace loading spinner component (#24525)
* Add loading spinner component

* Update some spinners

* Update some spinners

* Update indeterminate to ha-spinner

* add ha-spinner-delayed

* Remove ha-circular-progress component

* Update demo/src/custom-cards/ha-demo-card.ts

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

* Fix gallery

* Update set size

* Add ha-fade-in

* Remove wrong testing conditions

* Remove size number option

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-03-13 17:05:51 +01:00
Paul Bottein
e09dbb474b Add foundation for areas dashboard strategy (#24582)
* Add entity filter section strategy

* Rename parameters

* Add group support

* Add empty state

* Add area strategy

* Remove title

* Fix heading

* Add areas dashboard and views

* Use satisfies

* Remove unnecessary array copy

* Only define set if needed

* Sort area by name

* Fix sorting

* Use entity id

* Don't use section strategy in view

* Simplify view

* Remove section related changes
2025-03-13 17:03:35 +01:00
Grzegorz Libiszewski
ee10f9080d Unified safe area (insets) for Android and iOS (#23811)
* feat: Introduce new css variables for safe area

* feat: Replace all safe area env with variable

* fix: CR fix move from derived styles to styles and rename
2025-03-13 10:20:54 +02:00
renovate[bot]
4f9ec622bf Update dependency @babel/runtime to v7.26.10 [SECURITY] (#24615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-13 07:40:28 +00:00
renovate[bot]
dba269f2a3 Update dependency @bundle-stats/plugin-webpack-filter to v4.19.0 (#24605)
* Update dependency @bundle-stats/plugin-webpack-filter to v4.19.0

* fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-03-13 07:30:35 +00:00
karwosts
69026cbecf Energy self sufficiency gauge needs grid consumption (#24606) 2025-03-12 18:06:31 +01:00
Bram Kragten
dda7de3301 Fix issues with develop and serve (#24602)
* fix issues with develop and serve

* fix get image data, use hassUrl

* save picture-upload

* Update bundje.cjs

* Prettier

* Fix profile picture in dev serve mode

* person badge too

---------

Co-authored-by: Wendelin <w@pe8.at>
2025-03-12 16:59:40 +01:00
Wendelin
1e000d2740 Increase core start seconds (#24604) 2025-03-12 16:19:42 +01:00
Paulus Schoutsen
1d747c0901 Simplify CO2Signal check (#24566)
* Simplify CO2Signal check

* Remove more references.

* Update energy.ts
2025-03-12 08:31:12 +02:00
Norbert Rittel
8ef769559f Use proper capitalization for "WPA-PSK" (#24597)
This abbreviation is consistently referred to using upper-case letters (Wikipedia, e.g.)
2025-03-11 19:22:32 +01:00
karwosts
e141b4dbee Disable energy distribution animation if prefers-reduced-motion is set (#24581)
* Disable energy distribution animation if prefers-reduced-motion is set

* Move check to willUpdate. Remove circle circumference animation
2025-03-11 14:57:43 +01:00
puddly
d0545fe827 Update ZHA device websocket API types (#24087)
* Expose routing status from ZHA websocket API

* Clean up types a little bit

* Lint
2025-03-11 15:00:12 +02:00
renovate[bot]
4ef3a25479 Update dependency eslint to v9.22.0 (#24593)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-11 09:47:19 +02:00
Paul Bottein
616c1fda81 Rename entity filter to entity domain filter (#24587) 2025-03-10 13:34:15 -04:00
Paul Bottein
e8805be561 Perform action in slider and switch if it's a long press (#24579) 2025-03-10 15:30:29 +01:00
Darren Griffin
1b6d2ac08e Make element-preview sticky (#24580) 2025-03-10 14:07:29 +01:00
renovate[bot]
de4a8a0a72 Update dependency eslint-config-prettier to v10.1.1 (#24578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 11:46:03 +01:00
Paulus Schoutsen
58dd778b3d Add some type checking to demo (#24567) 2025-03-10 11:24:57 +01:00
karwosts
a4cdb294b1 Fix some issues with energy period update scheduling (#24563) 2025-03-10 08:46:05 +02:00
karwosts
07c4296771 Show statistics in history card on first load (#24554)
* Show statistics in history card on first load

* unnecessary line
2025-03-10 08:36:17 +02:00
renovate[bot]
95a99c7857 Update dependency terser-webpack-plugin to v5.3.14 (#24571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 08:33:11 +02:00
pisanvs
fee215fe96 Add missing margin to protection mode alert in addon info panel. (#24490)
* Add missing margin to protection mode alert in addon info panel.

* Only apply on the top level

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-03-10 06:29:17 +00:00
renovate[bot]
5e9341bf4e Update vitest monorepo to v3.0.8 (#24573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 08:19:02 +02:00
renovate[bot]
58b7c76b90 Update dependency terser-webpack-plugin to v5.3.13 (#24564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 21:32:23 +01:00
Paulus Schoutsen
6ed26407be Update country picker dialog in onboarding (#24551)
* Update country picker dialog in onboarding

* Handle if language has no locale

* Don't fall back to NL
2025-03-08 14:40:22 +00:00
dependabot[bot]
1ac0092d4e Bump axios from 1.7.9 to 1.8.2 (#24552)
Bumps [axios](https://github.com/axios/axios) from 1.7.9 to 1.8.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.9...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-07 19:37:07 +01:00
renovate[bot]
a4e8ea366a Update dependency @lokalise/node-api to v14 (#24547)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 18:18:04 +01:00
renovate[bot]
fe70355dde Update dependency typescript-eslint to v8.26.0 (#24533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 19:40:01 +01:00
renovate[bot]
01f07f6476 Update rspack monorepo to v1.2.7 (#24531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 18:49:15 +01:00
Paul Bottein
8fe55b9bc0 Only recreate stack editor when the type or index change (#24530) 2025-03-06 15:55:20 +01:00
renovate[bot]
913fdfd0eb Update dependency @codemirror/view to v6.36.4 (#24524)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 14:25:34 +02:00
Wendelin
c97916bea4 Landingpage add core checks before show errors (#24493)
* Check core only if supervisor or observer is unresponsive

* Improve core check

* Revert test code

* Remove unused prop

* Combine network info with core check

* Combine ping and network info

* Add 30 sec timeout before show errors

* Update landing-page/src/ha-landing-page.ts

* Assume supervisor update on failed ping

* Fix typo
2025-03-06 11:33:33 +01:00
Petar Petrov
cdfe4b53bf Ignore excessive keydown events in charts (#24523)
* Ignore excessive keydown events in charts

* lint
2025-03-06 11:30:17 +01:00
renovate[bot]
75edc5132b Update dependency prettier to v3.5.3 (#24521) 2025-03-06 07:37:43 +01:00
Petar Petrov
c79164992b Fix height of chart legend (#24519) 2025-03-05 14:05:01 +01:00
Petar Petrov
c581d6d028 Set chart axis pointer line to --info-color (#24494) 2025-03-05 11:04:56 +01:00
karwosts
d899711a48 Remember hidden energy devices from storage (#24470)
* Remember hidden devices from storage

* remove accidental

* typing fix
2025-03-05 11:38:13 +02:00
Paul Bottein
b7be74e722 Remove touch action none for toggle feature (#24514) 2025-03-05 10:25:00 +01:00
karwosts
e53961d395 No integer validation on ha-form-float (#24501) 2025-03-05 09:20:18 +01:00
Paulus Schoutsen
03b08fefb7 Support continue conversation in Assist dialog (#24511) 2025-03-05 09:11:29 +02:00
Jan-Philipp Benecke
79374f6052 Show script description in the more info dialog (#24507)
* Show script description in the more info dialog

* Use markdown
2025-03-05 09:07:47 +02:00
Stefan Agner
ba19849182 Avoid URL and email fields getting masked in add-on config view (#24509)
* Avoid URL and email fields getting masked in add-on config view

The backend will set "format" for add-on config options of type
"password", "url" and "email", to exactly these three values. Only
"password" fields should be masked though.

* lint

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-03-05 06:56:34 +00:00
renovate[bot]
48338e0886 Update Yarn to v4.7.0 (#24503)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 18:43:13 +01:00
renovate[bot]
3b87fc84a9 Update dependency core-js to v3.41.0 (#24504)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 18:42:43 +01:00
J. Nick Koston
61effc3f70 Add panel to show Bluetooth connection overview (#24463)
* Add panel to show Bluetooth connection overview

* tweak

* migrate to ha-button
2025-03-04 18:29:15 +01:00
Petar Petrov
e5b460c259 Tweak legend expand/collapse button (#24484)
* Tweak legend expand/collapse button

* Revert "Tweak legend expand/collapse button"

This reverts commit 5eafdad781.

* update UX
2025-03-04 14:51:54 +01:00
Joakim Sørensen
7a56731f56 Remove brackets (#24497) 2025-03-04 15:20:16 +02:00
Joakim Sørensen
bfe20d3760 Include name and version in already connected dialog if present (#24492)
* Include name and version in already connected dialog if present

* lint

* Apply suggestions from code review

* move
2025-03-04 11:39:29 +02:00
Paul Bottein
5a37087231 Use fire-event for copy, cut and duplicate (#24486)
* Add event for duplicate

* Update new events in card option and card edit mode

* Add copy event
2025-03-04 11:14:22 +02:00
renovate[bot]
dbe5bffe22 Update dependency babel-loader to v10 (#24472)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 09:05:30 +02:00
renovate[bot]
62da09d045 Update dependency typescript to v5.8.2 (#24487) 2025-03-03 18:11:07 +01:00
Wendelin
75d7676b36 No capitalization for quick bar commands (#24481)
Remove forces capitalization for quick bar commands
2025-03-03 12:53:36 +01:00
renovate[bot]
0bea89db91 Update vaadinWebComponents monorepo to v24.6.6 (#24482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 12:53:17 +01:00
Logan Rosen
7ad759dd95 Fix inconsistent color picker height across browsers (#24476)
Closes #24475
2025-03-03 11:17:46 +01:00
Bram Kragten
5377c9e75d Update currency for Zambia (#24480) 2025-03-03 11:00:50 +01:00
dependabot[bot]
bf206aa12b Bump home-assistant/wheels from 2024.11.0 to 2025.02.0 (#24478) 2025-03-03 08:16:03 +01:00
dependabot[bot]
73669e27f4 Bump actions/cache from 4.2.1 to 4.2.2 (#24479) 2025-03-03 08:13:18 +01:00
karwosts
1b5f4d3432 Add model_id filter to device selector (#23746)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-02 21:37:22 +01:00
renovate[bot]
49379b49d0 Update dependency terser-webpack-plugin to v5.3.12 (#24468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-02 14:36:26 +01:00
renovate[bot]
2827421c9f Update dependency @webcomponents/scoped-custom-element-registry to v0.0.10 (#24465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-02 09:39:12 +01:00
renovate[bot]
1a5a183410 Update dependency eslint-config-prettier to v10.0.2 (#24462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 20:04:57 +01:00
renovate[bot]
88a1de9aaf Update dependency @codemirror/search to v6.5.10 (#24459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 12:33:54 +01:00
renovate[bot]
4ad64ce2c8 Update dependency @bundle-stats/plugin-webpack-filter to v4.18.3 (#24458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 12:10:05 +02:00
renovate[bot]
3f1ca32d13 Update dependency element-internals-polyfill to v3.0.1 (#24457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-28 23:24:20 +01:00
renovate[bot]
a6f7ee6b28 Update rspack monorepo to v1.2.6 (#24448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-28 20:17:13 +01:00
Paul Bottein
0294198fba Correctly parse number state for numeric input card feature (#24453) 2025-02-28 16:46:53 +01:00
Norbert Rittel
ed6659ad8f More info panel: Replace "Dismiss dialog" tooltip with "Close info" (#24449)
* More info panel: Replace "Dismiss dialog" tooltip with "Close info"

Dismiss should only be used for closing dialogs like notifications that cannot be reopened.

Therefore the "Dismiss dialog" tooltip for the More info dialog is misleading, especially in translations that might emphasize the destructive meaning of "dismiss".

Matching the "Back to info" tooltip shown to return to the initial view from the entity settings this is changed to "Close info".

* use common close

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-02-28 15:06:03 +00:00
Paul Bottein
f1d04e5178 Add support for toggle card feature for automation domain (#24452) 2025-02-28 15:01:33 +00:00
Paul Bottein
807e87fce0 Use card text align variable for header text alignment (#24451)
Use card text align variable for header text aligment
2025-02-28 15:52:40 +01:00
Bram Kragten
cafab61727 Align common dialog translations (#24450) 2025-02-28 15:51:06 +01:00
karwosts
88e6906b6b More height fixes in devtools/statistics (#24438)
* More height fixes in devtools/statistics

* fix selection bar
2025-02-28 14:07:00 +01:00
Paul Bottein
ebc1259e39 Fix control number buttons height (#24441) 2025-02-28 14:05:14 +01:00
J. Nick Koston
c1f2e6d82b Small fixes for Bluetooth device info (#24436)
* Add missing service uuids to Bluetooth device info

Service UUIDs are different from Service data because
they do not have any data attached to them. I only
discovered that they were missing in the UI because
of helping a user troubleshoot of device, and we could
not find the Service UUID

* fix mapping

* fix mapping

* fix mapping

* tweaks
2025-02-28 10:03:15 +02:00
karwosts
ef964fd23e Change label on BT advertisement timestamp (#24439) 2025-02-28 08:04:02 +01:00
renovate[bot]
13105c2d6f Update dependency typescript-eslint to v8.25.0 (#24434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 20:43:33 +01:00
renovate[bot]
5b9262487d Update vitest monorepo to v3.0.7 (#24433)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 20:43:04 +01:00
renovate[bot]
4ec6e324f8 Update dependency element-internals-polyfill to v3 (#24364)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 20:42:16 +01:00
Paul Bottein
20f385e053 Use border color for focus state of button and select in dashboard (#24429) 2025-02-27 20:36:41 +01:00
Paul Bottein
dd441f882b Allow the card features buttons to be smaller if needed (#24431) 2025-02-27 19:30:15 +01:00
Bram Kragten
3a1d371b0b Don't show no config flow message when source = system (#24425)
dont show no config flow message when source = system
2025-02-27 16:15:42 +01:00
Paul Bottein
8848911b34 Use switch and add support for light, fan and valve (#24426)
* Use switch and add support for light and fan

* Add icon per domains
2025-02-27 15:35:12 +01:00
Paul Bottein
51193cf441 Reverse the order of all modes features and toggle (#24420)
Reverse the order of all modes features
2025-02-27 13:08:42 +01:00
Paul Bottein
a5b7f2466e Fix select box radio click on firefox (#24422) 2025-02-27 13:08:16 +01:00
Paul Bottein
3d9bde548d Don't show features settings if none is compatible (#24419) 2025-02-27 13:07:50 +01:00
Wendelin
dfa98a4ba8 Translate state in entity table (#24417) 2025-02-27 12:20:38 +01:00
Paul Bottein
20fe5b1b71 Fix header hidden when no badges (#24423) 2025-02-27 12:09:12 +01:00
Paul Bottein
9250ecb16f Add features position description in tile card editor (#24421) 2025-02-27 12:08:36 +01:00
Jan-Philipp Benecke
e21d1399ea Swap button positions of toggle feature (#24416) 2025-02-27 10:46:29 +01:00
Jan-Philipp Benecke
5dded38ccc Swap default positions of increment and decrement in counter actions feature (#24418) 2025-02-27 10:46:01 +01:00
renovate[bot]
54cc8f025c Update dependency barcode-detector to v3.0.1 (#24407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 07:54:00 +02:00
karwosts
0d215e65cd Remove duplicate translation key (#24414)
Update en.json
2025-02-27 07:46:54 +02:00
Paulus Schoutsen
f9824a3b3b Remove unused domain in history check (#24406) 2025-02-26 17:59:02 +01:00
Wendelin
197c9219bd Fix energy gauge tooltip (#24404) 2025-02-26 15:27:09 +01:00
Bram Kragten
4974a12221 Merge branch 'rc' into dev 2025-02-26 14:11:39 +01:00
Bram Kragten
a13bcac0aa Bumped version to 20250226.0 2025-02-26 14:10:36 +01:00
Bram Kragten
cf288f7cd1 Don't allow history to be picked for backup when db is not in default … (#24402) 2025-02-26 14:09:46 +01:00
Bram Kragten
1c8284609f Update translation keys for sub entry flows (#24400) 2025-02-26 13:38:21 +01:00
Petar Petrov
1fdadbf1b8 Add drag to zoom & double click in echarts (#24401) 2025-02-26 12:33:39 +01:00
karwosts
3b272ae411 Allow entity-filter-card to filter on other entity (#24396) 2025-02-25 22:37:31 +01:00
ildar170975
a048c36861 ha-tabs: adapt _affectScroll() to RTL (#24018)
* adapt _affectScroll for RTL

* eslint

* import "property"

* prettier

* Update ha-tabs.ts

This makes this work...

* Lint

---------

Co-authored-by: Yosi Levy <37745463+yosilevy@users.noreply.github.com>
2025-02-25 22:20:57 +01:00
Paul Bottein
2a6c1773f3 Update responsive layout svg (#24391) 2025-02-25 18:58:46 +01:00
Paul Bottein
bf17012753 Fix select box svg icon in dark mode (#24389) 2025-02-25 16:52:02 +01:00
Wendelin
bbaf23e049 Add lovelace bg opacity preview (#24387)
* Add lovelace bg opacity preview

* Add yaml preview

* Simplify logic with css var

* Clean up

* Remove important

* Add opacity default to 100
2025-02-25 16:21:32 +01:00
Jan Bouwhuis
34b7929165 Improve labels when there no mqtt entities or triggers debug info yet (#24383)
* Improve labels when there no mqtt entities or triggers debug info yet

* Update src/panels/config/devices/device-detail/integration-elements/mqtt/dialog-mqtt-device-debug-info.ts

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-02-25 15:11:19 +01:00
Paul Bottein
8f06e70a11 Add view top margin option for section view (#24386) 2025-02-25 15:06:13 +01:00
Jan-Philipp Benecke
50ac60b35e Improve area registry dialog user experience (#24086)
* Improve area registry dialog user experience

* Simplify expanded

* Improve heading and description

* Fix padding and remove secondary

* Process code review

* Remove more unused imports

* Re-add alias description
2025-02-25 15:04:30 +01:00
Petar Petrov
4caca19e32 Migrate sankey chart to echarts (#24185)
* Migrate sankey chart to echarts

* don't memoize options

* Dynamically load echart sankey component

* load Sankey component with the card

* load the sankey component in ha-chart-base
2025-02-25 15:02:41 +01:00
Wendelin
a7b1c45c00 Replace simple-tooltip with ha-tooltip (#24384)
* Start with simple-tooltip migration

* Remove simple-tooltip

* Fix tooltip in hassio-repositories

* Remove space

* Update hassio/src/dialogs/repositories/dialog-hassio-repositories.ts

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

* Update src/components/ha-icon-overflow-menu.ts

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

* Update src/components/ha-target-picker.ts

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

* Update src/components/media-player/ha-media-player-browse.ts

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

* Update src/components/ha-target-picker.ts

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

* Fix content props

* Use ha-tooltip in data-table-icon

* Update src/panels/config/areas/ha-config-area-page.ts

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

* Update src/panels/config/devices/ha-config-device-page.ts

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

* Update src/panels/config/integrations/ha-integration-card.ts

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

* Update src/panels/config/integrations/ha-integration-card.ts

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

* Update src/panels/config/integrations/ha-integration-list-item.ts

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

* Update src/panels/config/integrations/ha-integration-list-item.ts

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

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-02-25 14:58:38 +01:00
Paul Bottein
10c3e4c6f8 Rename appearance section to content in card/badge editors (#24385) 2025-02-25 14:56:54 +01:00
Petar Petrov
1f50c359dc Fix history Y axis with tiny values (#24342)
* Fix history Y axis with tiny values

* set min fractions to 2

* handle negative values
2025-02-25 12:00:33 +01:00
Paul Bottein
a3e24a3dc0 Add view header (#24237)
* Create basic section heading

* Update badge position

* Don't allow to move card

* Remove view badges in section

* Improve heading section UX

* Add basic editor for heading section

* Add badges position option

* Add layout and badge position

* Improve button

* Use layout

* Simplify edit mode

* Fix CSS

* Fix add heading button

* Improve badges

* Rename to extra space

* Fix add top badge position

* Add migration

* Fix delete section confirmation

* Add translations

* Update comment

* Add header config to view

* Add edit card overlay

* Remove badge support for sections

* Remove section badges

* Clean section

* Fix header visibility

* Update translations

* Add view header editor

* Use new markdown card option

* Revert useless changes

* Add options translations

* Use edit dialog

* Unify font

* Update default text

* Share default between editor and view

* Update src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts

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

* Update src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts

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

* Fix comment

* Use select box for view header editor

* Remove extra space option

* Update select box

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-02-25 11:43:54 +01:00
Bram Kragten
a906285a03 Use automatic_backups_configured flag to signal if backup onboardin… (#24367)
* Use `automatic_backups_configured` flag to signal if backup onboarding was done

* fix

* dont show a newly created password without showing it
2025-02-25 11:42:46 +01:00
Petar Petrov
5c933a43b2 Custom chart legend (#24227)
* Custom chart legend

* limit legend label length

* fade long legends

* tweak margin

* new design

* fix margins

* lighter background

* fix variable height charts

* tweak legend button

* lint

* switch to secondary-text-color

* Card option to expand legend

* Update src/components/chart/ha-chart-base.ts

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

* pr comments

* use ha-assist-chip

* pr comments

* Apply suggestions from code review

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

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-02-25 12:00:50 +02:00
Petar Petrov
151a7fbc40 Z-WaveJS: Delay fetching new config values after addition (#24317) 2025-02-25 10:44:45 +01:00
Petar Petrov
4fa915c869 Z-WaveJS: stop inclusion subscription while dialog is open (#24294)
* Z-WaveJS: stop inclusion subscription while dialog is open and handling it

* Don't pass dsk to core

* PR comments
2025-02-25 10:43:56 +01:00
Paul Bottein
d47e5c847b Add RTL and dark mode support for select box image (#24374)
Add rtl and dark for select box
2025-02-25 10:38:02 +01:00
Flo Edelmann
db5036aed3 Support limits in sensor card editor (#24358)
* Support `limits` in sensor card editor

* Add new translation keys for limit min/max
2025-02-25 09:19:13 +00:00
renovate[bot]
bb672d0272 Update dependency prettier to v3.5.2 (#24382)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 08:36:20 +02:00
renovate[bot]
e26d3d39f0 Update dependency eslint to v9.21.0 (#24381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 01:36:23 +01:00
renovate[bot]
e54c3a69af Update dependency @lokalise/node-api to v13.2.1 (#24377)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 19:59:53 +01:00
karwosts
cc04457d72 Add 'last seen' to BT advertisement montior (#24361)
* Add 'last seen' to BT advertisement montior

* use ha-relative-time
2025-02-24 17:29:34 +02:00
Wendelin
9e1d64e728 shoelace tooltip (#24337)
* Add shoelace based ha-tooltip

* Use shoelace component

* Improve styles

* Add docs

* Fix tooltip docs

* Revert new global styles
2025-02-24 15:37:59 +01:00
Paul Bottein
0cfe7f8d12 Tile card editor improvements (#24373)
* Add selector support

* Feedbacks

* Use select box fields in tile card editor
2025-02-24 14:26:55 +00:00
karwosts
2b1f301db6 Push map strategy logic down into map card (#24303) 2025-02-24 15:13:52 +01:00
Paul Bottein
fc4996412e Add select box component (#24370)
* Add select box component

* Add selector support

* Use it in markdown card

* Add select box to gallery

* Feedbacks
2025-02-24 15:07:51 +01:00
Jan-Philipp Benecke
ece4a6345f Use custom styling for cluster marker (#24371)
* Use custom styling for cluster marker

* Process code review
2025-02-24 13:11:06 +00:00
Bram Kragten
a4c08a78b9 Check for updated frontend on connect too (#24368) 2025-02-24 13:16:54 +02:00
Joakim Sørensen
a438fc5e41 Add connection check and dialog with results for cloud login (#24301) 2025-02-24 09:37:17 +01:00
karwosts
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]
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]
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 08:59:27 +02:00
dependabot[bot]
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 08:58:42 +02:00
renovate[bot]
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]
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
puddly
5eec814988 Hide hardware integrations from the "add integration" dialog (#24345) 2025-02-22 08:43:18 +02:00
Bram Kragten
4a1b7d46ca Merge branch 'rc' 2025-02-21 19:31:20 +01:00
Bram Kragten
75fadcca42 Bumped version to 20250221.0 2025-02-21 19:31:09 +01:00
Wendelin
41c93f5f7e Fix hassio backup restore url (#24313) 2025-02-21 19:30:04 +01:00
Petar Petrov
99559ff716 Enable downsampling in echarts (#24311)
* Enable downsampling in echarts

* remove unneeded symbol set
2025-02-21 19:27:32 +01:00
Wendelin
753fe719e3 Fix backup forever retention settings (#24299)
Fix forever retention settings
2025-02-21 19:27:31 +01:00
karwosts
5c14afd944 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 <MindFreeze@users.noreply.github.com>

* prettier

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-02-21 19:27:31 +01:00
Petar Petrov
23f1925c84 Make part of the chart rendering async for large datasets (#24260) 2025-02-21 19:27:30 +01:00
renovate[bot]
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]
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
Wendelin
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]
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 <32477463+silamon@users.noreply.github.com>
2025-02-20 19:36:32 +00:00
Wendelin
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
J. Nick Koston
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 <jan-philipp@bnck.me>

---------

Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
2025-02-20 20:09:51 +01:00
Jan-Philipp Benecke
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]
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 Benecke
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 Benecke
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 Bottein
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
Wendelin
fb1ee2ed1d Remove toggles from ha-icon-button (#24331) 2025-02-20 12:14:40 +01:00
Paul Bottein
9073282174 Add text only style to markdown card (#24329) 2025-02-20 11:40:39 +01:00
Jan-Philipp Benecke
91bd5cba08 Add switch toggle feature to tile card (#24325)
* Add tile switch toggle feature

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

* remove unneeded symbol set
2025-02-19 16:05:32 +02:00
Wendelin
2801d071ba Fix custom retention label (#24304) 2025-02-19 10:32:41 +01:00
Wendelin
71b65f208f Fix hassio backup restore url (#24313) 2025-02-19 10:32:15 +01:00
Paul Bottein
ab4efb7412 Fix cursor jump in light color pickers (#24312) 2025-02-19 10:30:24 +01:00
Logan Rosen
c7a46ec25b Improve ESLint config (#24290)
* Improve ESLint config
2025-02-18 17:30:36 +00:00
Jan-Philipp Benecke
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 Kragten
06932d1479 Prevent navigate when opening voice flow (#24300)
prevent navigate when opening voice flow
2025-02-18 14:27:59 +01:00
Wendelin
24211d5f25 Fix backup forever retention settings (#24299)
Fix forever retention settings
2025-02-18 14:05:04 +01:00
Bram Kragten
d387f19a31 Backup tweaks (#24165)
* Backup tweaks

* Show progress in fab

* Revert unused changes

---------

Co-authored-by: Wendelin <w@pe8.at>
2025-02-18 15:02:53 +02:00
Wendelin
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
karwosts
1363884773 Fix error handling/flickering in markdown card (#24280) 2025-02-18 08:16:10 +01:00
Adam Kapos
0256da511d Fix theme2hex with custom theme colors (#24282) 2025-02-18 08:04:46 +01:00
Petar Petrov
c52217c1ce Make part of the chart rendering async for large datasets (#24260) 2025-02-18 07:57:07 +01:00
karwosts
cdd17eed2e Fix untracked energy rendering at the base of the bar stack (#24288) 2025-02-18 06:36:53 +01:00
renovate[bot]
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]
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
Paul Bottein
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 <12148533+wendevlin@users.noreply.github.com>

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-02-18 06:33:17 +01:00
renovate[bot]
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
renovate[bot]
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]
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]
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
Paul Bottein
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]
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
karwosts
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 <MindFreeze@users.noreply.github.com>

* prettier

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-02-16 13:06:01 +00:00
renovate[bot]
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]
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
karwosts
84a0289e1b Use ha-md-button-menu in automation triggers/conditions (#24258) 2025-02-16 12:35:49 +01:00
renovate[bot]
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.net
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
karwosts
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
ildar170975
7524dc8709 Settings -> Helpers: make "Editable" columns sortable (#23976)
* "Editable" in "Helpers"

* prettier
2025-02-14 22:27:28 +01:00
renovate[bot]
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
Wendelin
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]
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 <32477463+silamon@users.noreply.github.com>
2025-02-14 20:27:21 +00:00
dependabot[bot]
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-14 21:08:23 +01:00
Wendelin
f1bb4a5694 Add title attribute to data-table column header (#24231) 2025-02-14 21:07:48 +01:00
dependabot[bot]
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-14 19:59:24 +00:00
dependabot[bot]
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-14 20:38:18 +01:00
renovate[bot]
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]
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-14 20:12:57 +01:00
Bram Kragten
c68002214f Merge branch 'rc' 2025-02-14 13:33:47 +01:00
Bram Kragten
8dbc203130 Bumped version to 20250214.0 2025-02-14 13:33:35 +01:00
Petar Petrov
64274d7355 Fix inclusion dialog in ZwaveJS panel (#24234) 2025-02-14 13:33:22 +01:00
Petar Petrov
c07f4de39d Fix endTime of statistics-chart (#24233) 2025-02-14 13:33:21 +01:00
Paulus Schoutsen
37ee2bf308 Fix config flow URLs linking to device (#24223) 2025-02-14 13:33:20 +01:00
Petar Petrov
d9559b7f07 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 13:33:20 +01:00
Paul Bottein
fce07daa20 Fix section border radius (#24159) 2025-02-14 13:33:19 +01:00
Brynley McDonald
5d6fcaf6bb Add Mastodon and Bluesky to help tip (#24099)
Add Mastodon and Bluesky to socials tip
2025-02-14 13:33:18 +01:00
Petar Petrov
0abccb88d6 Fix inclusion dialog in ZwaveJS panel (#24234) 2025-02-14 13:32:09 +01:00
renovate[bot]
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
karwosts
41df7a3f4a Keyboard accessibility for automation-action-row (convert M2->M3) (#24121) 2025-02-14 13:02:16 +01:00
Petar Petrov
920ec035c5 Fix endTime of statistics-chart (#24233) 2025-02-14 12:53:48 +01:00
Petar Petrov
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
ildar170975
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
ildar170975
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
ildar170975
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 Bottein
02deeb4ce7 Increase target zone for tile card icon click (#24219) 2025-02-14 08:52:13 +02:00
renovate[bot]
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 Schoutsen
abbf56db1d Fix config flow URLs linking to device (#24223) 2025-02-13 13:47:03 +00:00
Norbert Rittel
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 Costa
b66f41db7d Improve last backup status string (#24206) 2025-02-13 07:50:59 +01:00
renovate[bot]
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]
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]
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 Bottein
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
ildar170975
f25dac7f68 Settings -> Automations: show a title for "State" column (#23977)
show a title for "State" column
2025-02-12 09:46:40 +01:00
karwosts
99065a689f Retry subscribing to weather forecast if it fails (#24188) 2025-02-12 08:39:46 +02:00
renovate[bot]
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 Bottein
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 Bottein
78e2809fe7 Fix default value for color in entity badge editor (#24186) 2025-02-11 16:14:04 +02:00
renovate[bot]
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
ildar170975
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]
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
Wendelin
23a9ae6835 Onboarding restore use error code (#24172)
Use error code for incorrect password
2025-02-11 09:47:18 +01:00
Wendelin
dbd1e928de Make restore button destructive (#24173) 2025-02-11 09:37:56 +01:00
renovate[bot]
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 Kragten
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
Bram Kragten
0ab9098f23 Merge branch 'rc' 2025-02-10 19:39:11 +01:00
Bram Kragten
4498747fb1 Bumped version to 20250210.0 2025-02-10 19:38:30 +01:00
Petar Petrov
76977b64fa Bring back energy usage graph order (#24156) 2025-02-10 19:36:43 +01:00
Petar Petrov
2ca7395733 Limit max label width of hui-energy-devices-graph-card (#24152) 2025-02-10 19:36:42 +01:00
Petar Petrov
0900869957 Round log scale limits (#24151) 2025-02-10 19:36:41 +01:00
Petar Petrov
91e8750f44 Fix device energy card with max_devices (#24150) 2025-02-10 19:36:40 +01:00
karwosts
936f66c41c Stack solar forecasts (#24113) 2025-02-10 19:36:39 +01:00
Petar Petrov
9ab5be4730 Fix energy dashboard data formatting (#24101) 2025-02-10 19:36:38 +01:00
Petar Petrov
a30e501031 Set charts font to Roboto (#24097) 2025-02-10 19:36:37 +01:00
Wendelin
dcb04067b8 Add network adapter translations (#24096) 2025-02-10 19:36:37 +01:00
Petar Petrov
bf962b29af Reduce padding in energy charts and align unit (#24095) 2025-02-10 19:36:36 +01:00
Jan-Philipp Benecke
0ae6fa0763 Fix area registry dialog field (#24090) 2025-02-10 19:36:35 +01:00
Wendelin
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 <mail@bramkragten.nl>

* Show backup type not at onboarding

* Only show backup type in correct translationPanel

* Fix quotes

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-02-10 16:40:08 +01:00
Paulus Schoutsen
44cc75afbc Require opt-in config flow navigateToResult (#24120) 2025-02-10 15:58:10 +01:00
Petar Petrov
748642a8d6 Limit max label width of hui-energy-devices-graph-card (#24152) 2025-02-10 15:54:20 +01:00
Petar Petrov
3d5c65d652 Bring back energy usage graph order (#24156) 2025-02-10 15:53:36 +01:00
Paul Bottein
a26bf80b13 Fix section border radius (#24159) 2025-02-10 15:53:05 +01:00
Petar Petrov
497c6c35f1 Fix device energy card with max_devices (#24150) 2025-02-10 10:12:42 +01:00
Petar Petrov
b0b06a2787 Round log scale limits (#24151) 2025-02-10 10:12:12 +01:00
Paulus Schoutsen
f3d55447ca Add support for intent-progress assist events (#24143) 2025-02-09 23:36:56 -05:00
renovate[bot]
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 Schoutsen
6ec4041c4c Navigate to newly created config entry (#24109) 2025-02-07 08:32:21 +02:00
karwosts
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
karwosts
af7bb85667 Stack solar forecasts (#24113) 2025-02-07 06:44:39 +01:00
renovate[bot]
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]
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]
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 Petrov
2a0f69a629 Fix energy dashboard data formatting (#24101) 2025-02-06 12:38:22 +01:00
Petar Petrov
9411a77f14 Set charts font to Roboto (#24097) 2025-02-06 13:16:35 +02:00
Petar Petrov
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 McDonald
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 Grimes
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 Petrov
bdbc9bc1b4 Reduce padding in energy charts and align unit (#24095) 2025-02-06 09:36:58 +01:00
renovate[bot]
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 <MindFreeze@users.noreply.github.com>
2025-02-06 07:53:26 +00:00
Wendelin
3918194d2d Add network adapter translations (#24096) 2025-02-06 09:49:05 +02:00
Charles Garwood
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 Benecke
35face602b Fix area registry dialog field (#24090) 2025-02-05 18:33:00 +01:00
Bram Kragten
9d7d332790 20250205.0 (#24088) 2025-02-05 16:38:06 +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 Petrov
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 Petrov
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 Petrov
f2e35dc70a Fix chart preview (#24080)
* Fix chart preview

* Revert change to timeline-chart labels
2025-02-05 16:20:18 +01:00
Petar Petrov
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 Bottein
553bb61db7 Fix statistic chart tooltip values (#24074) 2025-02-05 16:14:58 +01:00
Petar Petrov
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 Kragten
28b3f2970a Use max of width and actualBoundingBox to get text width (#24085) 2025-02-05 17:12:48 +02:00
Petar Petrov
7d170a710e Disable chart update animation (#24084) 2025-02-05 15:04:03 +00:00
Bram Kragten
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 Kragten
b6eaff46e9 Chart: Add tooltip styling to theme (#24082) 2025-02-05 16:37:13 +02:00
Bram Kragten
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 Petrov
6ff018afc9 Fix chart preview (#24080)
* Fix chart preview

* Revert change to timeline-chart labels
2025-02-05 15:16:16 +01:00
Bram Kragten
ad48732bb7 Charts: add styles for legend page controls (#24081) 2025-02-05 16:04:14 +02:00
Bram Kragten
fef162346a Set list color of update more info to dialog background (#24076) 2025-02-05 12:48:13 +01:00
Bram Kragten
72d208d1ac Fix label truncated timeline chart (#24077) 2025-02-05 12:47:39 +01:00
Petar Petrov
5a8b1b0fd4 Fix device energy bar chart (#24079) 2025-02-05 12:47:21 +01:00
Bram Kragten
4cfc651799 Fix condition in tracing graph (#24075) 2025-02-05 10:04:39 +00:00
Petar Petrov
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 Bottein
f0507a88a6 Fix statistic chart tooltip values (#24074) 2025-02-05 11:18:29 +02:00
renovate[bot]
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 Petrov
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 Bottein
ce0f02a45b Display unavailable backups locations (#24058)
Display anavailable backups locations
2025-02-04 18:21:27 +01:00
Paul Bottein
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 Bottein
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 Kragten
904ee2e418 Add support package download to cloud (#24051) 2025-02-04 18:06:41 +01:00
Paul Bottein
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 Bottein
3a12019b64 Display unavailable backups locations (#24058)
Display anavailable backups locations
2025-02-04 14:45:38 +01:00
Bram Kragten
6c2cf1ff60 Dont show voice wizard for voip (#24050)
dont show voice wizard for voip
2025-02-04 14:38:03 +02:00
Petar Petrov
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 Benecke
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 Bottein
7dbc78f1d6 Improve value formatting inside backup tooltip (#24057) 2025-02-04 13:47:24 +02:00
Ingolf Becker
f965a3504f Improve weather forecast card layout (#23704)
* Improve weather forecast card layout

* fix units and improve naming

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-02-04 06:32:30 +00:00
karwosts
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 Petrov
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 Bottein
b263b74916 Increase margin to avoid fab overlap on backup overview page (#24047) 2025-02-03 18:20:16 +01:00
Paul Bottein
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 Bottein
72df585c5e Fix download unencrypted backup logic (#24045) 2025-02-03 18:20:15 +01:00
Petar Petrov
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 Petrov
6eb43a7d61 Workaround for chart size bug in editor preview (#24040) 2025-02-03 18:20:13 +01:00
Petar Petrov
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 Petrov
0d50d2664f Fix legend in charts (#24025)
* Fix legend in line charts

* fix statistics graph legend
2025-02-03 18:20:11 +01:00
Philipp
ff1159402e Fix browser media player showing more info dialog (#24021) 2025-02-03 18:20:11 +01:00
Jan-Philipp Benecke
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
ildar170975
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 Petrov
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
karwosts
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 Bottein
863ff622be Increase margin to avoid fab overlap on backup overview page (#24047) 2025-02-03 16:00:08 +01:00
Petar Petrov
730cea6646 Workaround for chart size bug in editor preview (#24040) 2025-02-03 15:59:43 +01:00
karwosts
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 <MindFreeze@users.noreply.github.com>

* less async

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-02-03 16:13:20 +02:00
Paul Bottein
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 Bottein
37ac796c8f Fix download unencrypted backup logic (#24045) 2025-02-03 14:40:49 +01:00
Petar Petrov
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 Bottein
173725f011 Use ignoreDiacritics in fuse library (#24041) 2025-02-03 12:53:34 +01:00
Petar Petrov
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
Philipp
d77bdf4ac6 Fix browser media player showing more info dialog (#24021) 2025-02-03 11:36:19 +01:00
Petar Petrov
ac3796ec31 Fix chart height (#24028) 2025-02-03 11:21:30 +01:00
Petar Petrov
8c3fdfb6fb Fix legend in charts (#24025)
* Fix legend in line charts

* fix statistics graph legend
2025-02-03 11:09:59 +01:00
karwosts
b7c7d0b4b5 Scroll todo list if it overflows grid_layout (#24000) 2025-02-03 09:09:01 +02:00
ildar170975
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]
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 Kragten
97dfccf4c7 Make date period picker respect timezone settings (#23996) 2025-02-01 17:32:07 +01:00
Jan-Philipp Benecke
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 Petrov
6b691063a8 Hide "heating" data from climate charts (#23997) 2025-01-31 18:19:44 +01:00
Paul Bottein
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 Petrov
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 Petrov
3769f8c7c0 Hide irrelevant errors from echarts zoom (#23992) 2025-01-31 18:19:41 +01:00
Paul Bottein
f0a56e75f5 Improve encrypted backup dialog (#23991)
* Improve encrypted backup dialog

* Remove unused code
2025-01-31 18:19:40 +01:00
Petar Petrov
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 Petrov
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 Petrov
684cd0f627 Fix legend resetting on zoom (#23985) 2025-01-31 18:19:38 +01:00
Paul Bottein
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 Petrov
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 Bottein
251e6399f5 Reduce chart height to 300px (#23979) 2025-01-31 18:19:36 +01:00
karwosts
f44c5d7a63 Improve statistics graph axis when using energy_date_selection (#23974) 2025-01-31 18:19:35 +01:00
ildar170975
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 Petrov
f8de2c64a5 Hide "heating" data from climate charts (#23997) 2025-01-31 17:13:13 +00:00
Bram Kragten
34ef5be720 fix time input width (#23998) 2025-01-31 17:12:49 +00:00
Petar Petrov
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 Bottein
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 Bottein
d4497ca39c Improve encrypted backup dialog (#23991)
* Improve encrypted backup dialog

* Remove unused code
2025-01-31 16:10:43 +00:00
Petar Petrov
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 Petrov
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 Petrov
87dfed4beb Hide irrelevant errors from echarts zoom (#23992) 2025-01-31 15:37:47 +01:00
Wendelin
dae991dc89 Improve develop and serve (#23990) 2025-01-31 15:50:15 +02:00
Paul Bottein
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 Petrov
b2a6c8bd36 Fix legend resetting on zoom (#23985) 2025-01-31 11:13:46 +01:00
Petar Petrov
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]
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 Bottein
b15b577057 Reduce chart height to 300px (#23979) 2025-01-31 10:42:51 +02:00
karwosts
653aeae3d8 Improve statistics graph axis when using energy_date_selection (#23974) 2025-01-31 08:45:37 +02:00
ildar170975
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]
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 Bottein
c337bc5f97 Improve backup settings display on mobile (#23967) 2025-01-30 18:07:52 +01:00
Petar Petrov
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 Bottein
52e9bc3213 Fix backup location config not updated (#23965) 2025-01-30 18:07:50 +01:00
Paul Bottein
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 Petrov
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 Bottein
10498ce18d Display device name in bluetooth panel (#23960) 2025-01-30 18:07:48 +01:00
Wendelin
6a5936b2b2 Add correct link to backup.create_automatic (#23959) 2025-01-30 18:07:47 +01:00
Norbert Rittel
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 Bottein
e7931ce049 Restore scroll position go back to backup settings page (#23955) 2025-01-30 18:07:46 +01:00
ildar170975
59b2582fe3 Fix for "Increase generic entity row touch target" (#23953)
fix for "touch target"
2025-01-30 18:07:45 +01:00
karwosts
8577b0721c Fix untracked energy in compare (#23949) 2025-01-30 18:07:44 +01:00
J. Nick Koston
91319be855 Reduce size of address column on Bluetooth Advertisement monitor (#23942) 2025-01-30 18:07:43 +01:00
Simon Lamon
0dff538298 Backup location translations improvements (#23940)
* Backup location translations improvements

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

* Apply better translations
2025-01-30 18:06:57 +01:00
Norbert Rittel
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 Bottein
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 Bottein
ce58962dbb Fix backup location config not updated (#23965) 2025-01-30 17:43:39 +01:00
Petar Petrov
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 Bottein
a29544c1e6 Improve backup settings display on mobile (#23967) 2025-01-30 17:49:05 +02:00
Petar Petrov
b2b71edd04 Use CSS variables to theme echarts (#23963)
* Use CSS variables to theme echarts

* fix styles
2025-01-30 14:39:59 +01:00
ildar170975
028472fc7b Fix for "Increase generic entity row touch target" (#23953)
fix for "touch target"
2025-01-30 13:26:11 +01:00
Paul Bottein
b056ce228b Display device name in bluetooth panel (#23960) 2025-01-30 12:36:10 +01:00
Wendelin
0cd4256c0e Add correct link to backup.create_automatic (#23959) 2025-01-30 11:05:33 +00:00
Yosi Levy
e274c5b23f Add node memory to allow commit (#23954) 2025-01-30 11:06:50 +02:00
karwosts
ea57846465 Fix untracked energy in compare (#23949) 2025-01-30 09:57:54 +01:00
Paul Bottein
3f2e2bc659 Restore scroll position go back to backup settings page (#23955) 2025-01-30 09:56:52 +01:00
J. Nick Koston
e3f2f66206 Reduce size of address column on Bluetooth Advertisement monitor (#23942) 2025-01-29 19:01:47 +00:00
485 changed files with 16230 additions and 6748 deletions

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
.devcontainer/post_create.sh Executable file
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."

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/cache@v4.2.0
uses: actions/cache@v4.2.2
with:
path: |
node_modules/.cache/prettier
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.6.0
uses: actions/upload-artifact@v4.6.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/upload-artifact@v4.6.0
uses: actions/upload-artifact@v4.6.1
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.6.0
uses: actions/upload-artifact@v4.6.1
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.6.0
uses: actions/upload-artifact@v4.6.1
with:
name: translations
path: translations.tar.gz

View File

@@ -74,7 +74,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels
uses: home-assistant/wheels@2024.11.0
uses: home-assistant/wheels@2025.02.0
with:
abi: cp313
tag: musllinux_1_2

42
.vscode/tasks.json vendored
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"
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.6.0.cjs
yarnPath: .yarn/releases/yarn-4.7.0.cjs

View File

@@ -55,7 +55,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__STATIC_PATH__: "/static/",
__HASS_URL__: `\`${
"HASS_URL" in process.env
? process.env["HASS_URL"]
? process.env.HASS_URL
: "${location.protocol}//${location.host}"
}\``,
"process.env.NODE_ENV": JSON.stringify(

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",
},
];
});

View File

@@ -56,6 +56,7 @@ const getCommonTemplateVars = () => {
);
return {
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
hassUrl: process.env.HASS_URL || "",
};
};

View File

@@ -90,6 +90,10 @@ function copyMapPanel(staticDir) {
npmPath("leaflet/dist/leaflet.css"),
staticPath("images/leaflet/")
);
copyFileDir(
npmPath("leaflet.markercluster/dist/MarkerCluster.css"),
staticPath("images/leaflet/")
);
fs.copySync(
npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/")

View File

@@ -3,7 +3,7 @@ const path = require("path");
const rspack = require("@rspack/core");
const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin");
const { StatsWriterPlugin } = require("webpack-stats-plugin");
const filterStats = require("@bundle-stats/plugin-webpack-filter").default;
const filterStats = require("@bundle-stats/plugin-webpack-filter");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
const log = require("fancy-log");

View File

@@ -5,7 +5,7 @@ import { until } from "lit/directives/until";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import "../../../src/components/ha-circular-progress";
import "../../../src/components/ha-spinner";
import type { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import type {
@@ -44,9 +44,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
<div class="picker">
<div class="label">
${this._switching
? html`
<ha-circular-progress indeterminate></ha-circular-progress>
`
? html`<ha-spinner></ha-spinner>`
: until(
selectedDemoConfig.then(
(conf) => html`

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,

View File

@@ -1,9 +1,10 @@
import type { validateConfig } from "../../../src/data/config";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfig = (hass: MockHomeAssistant) => {
hass.mockWS("validate_config", () => ({
actions: { valid: true },
conditions: { valid: true },
triggers: { valid: true },
hass.mockWS<typeof validateConfig>("validate_config", () => ({
actions: { valid: true, error: null },
conditions: { valid: true, error: null },
triggers: { valid: true, error: null },
}));
};

View File

@@ -1,19 +1,26 @@
import type { getConfigEntries } from "../../../src/data/config_entries";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfigEntries = (hass: MockHomeAssistant) => {
hass.mockWS("config_entries/get", () => ({
entry_id: "co2signal",
domain: "co2signal",
title: "Electricity Maps",
source: "user",
state: "loaded",
supports_options: false,
supports_remove_device: false,
supports_unload: true,
supports_reconfigure: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
}));
hass.mockWS<typeof getConfigEntries>("config_entries/get", () => [
{
entry_id: "mock-entry-co2signal",
domain: "co2signal",
title: "Electricity Maps",
source: "user",
state: "loaded",
supports_options: false,
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,
reason: null,
num_subentries: 0,
error_reason_translation_key: null,
error_reason_translation_placeholders: null,
},
]);
};

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",
},
},
];
}
);

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",
},
];
});

View File

@@ -0,0 +1,10 @@
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="64" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="63" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="black" fill-opacity="0.12"/>
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="black" fill-opacity="0.12"/>
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,7 @@
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="black" fill-opacity="0.12"/>
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="black" fill-opacity="0.12"/>
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="black" fill-opacity="0.12"/>
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="black" fill-opacity="0.12"/>
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 964 B

View File

@@ -1,63 +0,0 @@
import type { TemplateResult } from "lit";
import { html, css, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-bar";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-circular-progress";
import "@material/web/progress/circular-progress";
import type { HomeAssistant } from "../../../../src/types";
@customElement("demo-components-ha-circular-progress")
export class DemoHaCircularProgress extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render(): TemplateResult {
return html`<ha-card header="Basic circular progress">
<div class="card-content">
<ha-circular-progress indeterminate></ha-circular-progress></div
></ha-card>
<ha-card header="Different circular progress sizes">
<div class="card-content">
<ha-circular-progress
indeterminate
size="tiny"
></ha-circular-progress>
<ha-circular-progress
indeterminate
size="small"
></ha-circular-progress>
<ha-circular-progress
indeterminate
size="medium"
></ha-circular-progress>
<ha-circular-progress
indeterminate
size="large"
></ha-circular-progress></div
></ha-card>
<ha-card header="Circular progress with an aria-label">
<div class="card-content">
<ha-circular-progress
indeterminate
aria-label="Doing something..."
></ha-circular-progress>
<ha-circular-progress
indeterminate
.ariaLabel=${"Doing something..."}
></ha-circular-progress></div
></ha-card>`;
}
static styles = css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-circular-progress": DemoHaCircularProgress;
}
}

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,

View File

@@ -0,0 +1,3 @@
---
title: Select box
---

View File

@@ -0,0 +1,152 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-select-box";
import type { SelectBoxOption } from "../../../../src/components/ha-select-box";
const basicOptions: SelectBoxOption[] = [
{
value: "text-only",
label: "Text only",
},
{
value: "card",
label: "Card",
},
{
value: "disabled",
label: "Disabled option",
disabled: true,
},
];
const fullOptions: SelectBoxOption[] = [
{
value: "text-only",
label: "Text only",
description: "Only text, no border and background",
image: "/images/select_box/text_only.svg",
},
{
value: "card",
label: "Card",
description: "With border and background",
image: "/images/select_box/card.svg",
},
{
value: "disabled",
label: "Disabled",
description: "Option that can not be selected",
disabled: true,
},
];
const selects: {
id: string;
label: string;
class?: string;
options: SelectBoxOption[];
disabled?: boolean;
}[] = [
{
id: "basic",
label: "Basic",
options: basicOptions,
},
{
id: "full",
label: "With description and image",
options: fullOptions,
},
];
@customElement("demo-components-ha-select-box")
export class DemoHaSelectBox extends LitElement {
@state() private value?: string = "off";
handleValueChanged(e: CustomEvent) {
this.value = e.detail.value as string;
}
protected render(): TemplateResult {
return html`
${repeat(selects, (select) => {
const { id, label, options } = select;
return html`
<ha-card>
<div class="card-content">
<label id=${id}>${label}</label>
<ha-select-box
.value=${this.value}
.options=${options}
@value-changed=${this.handleValueChanged}
>
</ha-select-box>
</div>
</ha-card>
`;
})}
<ha-card>
<div class="card-content">
<p class="title"><b>Column layout</b></p>
<div class="vertical-selects">
${repeat(selects, (select) => {
const { options } = select;
return html`
<ha-select-box
.value=${this.value}
.options=${options}
.maxColumns=${1}
@value-changed=${this.handleValueChanged}
>
</ha-select-box>
`;
})}
</div>
</div>
</ha-card>
`;
}
static styles = css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
pre {
margin-top: 0;
margin-bottom: 8px;
}
p {
margin: 0;
}
label {
font-weight: 600;
margin-bottom: 8px;
display: block;
}
.custom {
--mdc-icon-size: 24px;
--control-select-color: var(--state-fan-active-color);
--control-select-thickness: 130px;
--control-select-border-radius: 36px;
}
p.title {
margin-bottom: 12px;
}
.vertical-selects ha-select-box {
display: block;
margin-bottom: 24px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-select-box": DemoHaSelectBox;
}
}

View File

@@ -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,

View File

@@ -1,4 +1,4 @@
---
title: Circular Progress
title: Spinner
subtitle: Can be used to indicate an ongoing task.
---

View File

@@ -0,0 +1,44 @@
import type { TemplateResult } from "lit";
import { html, css, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-bar";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-spinner";
import type { HomeAssistant } from "../../../../src/types";
@customElement("demo-components-ha-spinner")
export class DemoHaSpinner extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render(): TemplateResult {
return html`<ha-card header="Basic spinner">
<div class="card-content">
<ha-spinner></ha-spinner></div
></ha-card>
<ha-card header="Different spinner sizes">
<div class="card-content">
<ha-spinner size="tiny"></ha-spinner>
<ha-spinner size="small"></ha-spinner>
<ha-spinner size="medium"></ha-spinner>
<ha-spinner size="large"></ha-spinner></div
></ha-card>
<ha-card header="Spinner with an aria-label">
<div class="card-content">
<ha-spinner aria-label="Doing something..."></ha-spinner>
<ha-spinner .ariaLabel=${"Doing something..."}></ha-spinner></div
></ha-card>`;
}
static styles = css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-spinner": DemoHaSpinner;
}
}

View File

@@ -0,0 +1,30 @@
---
title: Tooltip
---
A tooltip's target is its _first child element_, so you should only wrap one element inside of the tooltip. If you need the tooltip to show up for multiple elements, nest them inside a container first.
Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout.
<ha-tooltip content="This is a tooltip">
<ha-button>Hover Me</ha-button>
</ha-tooltip>
```
<ha-tooltip content="This is a tooltip">
<ha-button>Hover Me</ha-button>
</ha-tooltip>
```
## Documentation
This element is based on sholace `sl-tooltip` it only sets some css tokens and has a custom show/hide animation.
<a href="https://shoelace.style/components/tooltip" target="_blank" rel="noopener noreferrer">Shoelace documentation</a>
### HA style tokens
In your theme settings use this without the prefixed `--`.
- `--ha-tooltip-border-radius` (Default: 4px)
- `--ha-tooltip-arrow-size` (Default: 8px)

View File

@@ -0,0 +1,2 @@
import "../../../../src/components/ha-tooltip";
import "../../../../src/components/ha-button";

View File

@@ -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",

View File

@@ -1,7 +1,7 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-spinner";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
@@ -21,7 +21,7 @@ class HassioAddonConfigDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
return html`<ha-spinner></ha-spinner>`;
}
const hasConfiguration =
(this.addon.options && Object.keys(this.addon.options).length) ||

View File

@@ -113,8 +113,9 @@ class HassioAddonConfig extends LitElement {
required: entry.required,
selector: {
text: {
type:
entry.format || MASKED_FIELDS.includes(entry.name)
type: entry.format
? entry.format
: MASKED_FIELDS.includes(entry.name)
? "password"
: "text",
},

View File

@@ -2,7 +2,7 @@ import "../../../../src/components/ha-card";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-spinner";
import "../../../../src/components/ha-markdown";
import { customElement, property, state } from "lit/decorators";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
@@ -33,7 +33,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
return html`<ha-spinner></ha-spinner>`;
}
return html`
<div class="content">

View File

@@ -11,7 +11,6 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-circular-progress";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import {
fetchAddonInfo,

View File

@@ -1,7 +1,7 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-spinner";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
@@ -23,7 +23,7 @@ class HassioAddonInfoDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
return html`<ha-spinner></ha-spinner>`;
}
return html`

View File

@@ -1331,6 +1331,12 @@ class HassioAddonInfo extends LitElement {
ha-alert mwc-button {
--mdc-theme-primary: var(--primary-text-color);
}
:host > ha-alert {
display: block;
margin-bottom: 16px;
}
a {
text-decoration: none;
}

View File

@@ -6,7 +6,7 @@ import {
type TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-spinner";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
@@ -28,9 +28,7 @@ class HassioAddonLogDashboard extends LitElement {
protected render(): TemplateResult {
if (!this.addon) {
return html`
<ha-circular-progress indeterminate></ha-circular-progress>
`;
return html` <ha-spinner></ha-spinner> `;
}
return html`
<div class="search">

View File

@@ -253,13 +253,9 @@ export class HassioBackups extends LitElement {
"backup.delete_selected"
)}
.path=${mdiDelete}
id="delete-btn"
class="warning"
@click=${this._deleteSelected}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="delete-btn">
${this.supervisor.localize("backup.delete_selected")}
</simple-tooltip>
`}
</div>
</div> `

View File

@@ -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);
}

View File

@@ -3,7 +3,6 @@ import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-circular-progress";
import "../../../src/components/ha-file-upload";
import type { HassioBackup } from "../../../src/data/hassio/backup";
import { uploadBackup } from "../../../src/data/hassio/backup";
@@ -14,7 +13,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 +69,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",

View File

@@ -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}

View File

@@ -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>

View File

@@ -12,6 +12,7 @@ import "../../../../src/components/ha-md-dialog";
import "../../../../src/components/ha-dialog-header";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-spinner";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-header-bar";
@@ -35,7 +36,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 +43,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 +62,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 +86,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 +99,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}
@@ -142,7 +139,7 @@ class HassioBackupDialog
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: this._restoringBackup
? html`<div class="loading">
<ha-circular-progress indeterminate></ha-circular-progress>
<ha-spinner></ha-spinner>
</div>`
: html`
<supervisor-backup-content
@@ -150,7 +147,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 +157,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 +192,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 +227,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 +287,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 +303,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 {
@@ -310,10 +311,6 @@ class HassioBackupDialog
haStyle,
haStyleDialog,
css`
ha-circular-progress {
display: block;
text-align: center;
}
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);

View File

@@ -5,6 +5,7 @@ import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-spinner";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import {
createHassioFullBackup,
@@ -58,7 +59,7 @@ class HassioCreateBackupDialog extends LitElement {
)}
>
${this._creatingBackup
? html`<ha-circular-progress indeterminate></ha-circular-progress>`
? html`<ha-spinner></ha-spinner>`
: html`<supervisor-backup-content
.hass=${this.hass}
.supervisor=${this._dialogParams.supervisor}
@@ -142,10 +143,6 @@ class HassioCreateBackupDialog extends LitElement {
:host {
direction: var(--direction);
}
ha-circular-progress {
display: block;
text-align: center;
}
`,
];
}

View File

@@ -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 = (

View File

@@ -4,7 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-spinner";
import "../../../../src/components/ha-select";
import "../../../../src/components/ha-dialog";
import {
@@ -69,12 +69,7 @@ class HassioDatadiskDialog extends LitElement {
?hideActions=${this.moving}
>
${this.moving
? html` <ha-circular-progress
aria-label="Moving"
size="large"
indeterminate
>
</ha-circular-progress>
? html`<ha-spinner aria-label="Moving" size="large"></ha-spinner>
<p class="progress-text">
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.moving_desc"
@@ -166,7 +161,7 @@ class HassioDatadiskDialog extends LitElement {
ha-select {
width: 100%;
}
ha-circular-progress {
ha-spinner {
display: block;
margin: 32px;
text-align: center;

View File

@@ -10,7 +10,7 @@ import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-spinner";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield";
@@ -161,12 +161,8 @@ export class DialogHassioNetwork
.disabled=${this._scanning}
>
${this._scanning
? html`<ha-circular-progress
aria-label="Scanning"
indeterminate
size="small"
>
</ha-circular-progress>`
? html`<ha-spinner aria-label="Scanning" size="small">
</ha-spinner>`
: this.supervisor.localize("dialog.network.scan_ap")}
</mwc-button>
${this._accessPoints &&
@@ -282,8 +278,7 @@ export class DialogHassioNetwork
</mwc-button>
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
${this._processing
? html`<ha-circular-progress indeterminate size="small">
</ha-circular-progress>`
? html`<ha-spinner size="small"> </ha-spinner>`
: this.supervisor.localize("common.save")}
</mwc-button>
</div>`;

View File

@@ -1,6 +1,5 @@
import "@material/mwc-button/mwc-button";
import { mdiDelete, mdiDeleteOff } from "@mdi/js";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -8,7 +7,8 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-tooltip";
import "../../../../src/components/ha-spinner";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-icon-button";
import type {
@@ -118,28 +118,27 @@ class HassioRepositoriesDialog extends LitElement {
<div>${repo.maintainer}</div>
<div>${repo.url}</div>
</div>
<div class="delete" slot="end">
<ha-icon-button
.disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug)
? mdiDeleteOff
: mdiDelete}
@click=${this._removeRepository}
>
</ha-icon-button>
<simple-tooltip
animation-delay="0"
position="bottom"
offset="1"
>
${this._dialogParams!.supervisor.localize(
usedRepositories.includes(repo.slug)
? "dialog.repositories.used"
: "dialog.repositories.remove"
)}
</simple-tooltip>
</div>
<ha-tooltip
class="delete"
slot="end"
.content=${this._dialogParams!.supervisor.localize(
usedRepositories.includes(repo.slug)
? "dialog.repositories.used"
: "dialog.repositories.remove"
)}
>
<div>
<ha-icon-button
.disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug)
? mdiDeleteOff
: mdiDelete}
@click=${this._removeRepository}
>
</ha-icon-button>
</div>
</ha-tooltip>
</ha-md-list-item>
`
)
@@ -162,10 +161,7 @@ class HassioRepositoriesDialog extends LitElement {
></ha-textfield>
<mwc-button @click=${this._addRepository}>
${this._processing
? html`<ha-circular-progress
indeterminate
size="small"
></ha-circular-progress>`
? html`<ha-spinner size="small"></ha-spinner>`
: this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
@@ -203,7 +199,7 @@ class HassioRepositoriesDialog extends LitElement {
margin-inline-start: 8px;
margin-inline-end: initial;
}
ha-circular-progress {
ha-spinner {
display: block;
margin: 32px;
text-align: center;

View File

@@ -15,6 +15,7 @@ import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-spinner";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded";
import "../../../src/components/ha-icon-button";
@@ -192,12 +193,10 @@ class UpdateAvailableCard extends LitElement {
`
: nothing}
`
: html`<ha-circular-progress
: html`<ha-spinner
aria-label="Updating"
size="large"
indeterminate
>
</ha-circular-progress>
></ha-spinner>
<p class="progress-text">
${this.supervisor.localize("update_available.updating", {
name: this._name,
@@ -465,7 +464,7 @@ class UpdateAvailableCard extends LitElement {
justify-content: space-between;
}
ha-circular-progress {
ha-spinner {
display: block;
margin: 32px;
text-align: center;

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"];

View File

@@ -22,6 +22,8 @@ import {
import { fireEvent } from "../../../src/common/dom/fire_event";
import { fileDownload } from "../../../src/util/file_download";
import { getSupervisorLogs, getSupervisorLogsFollow } from "../data/supervisor";
import { waitForSeconds } from "../../../src/common/util/wait";
import { ASSUME_CORE_START_SECONDS } from "../ha-landing-page";
const ERROR_CHECK = /^[\d\s-:]+(ERROR|CRITICAL)(.*)/gm;
declare global {
@@ -216,7 +218,7 @@ class LandingPageLogs extends LitElement {
// eslint-disable-next-line no-console
console.error(err);
// fallback to observerlogs if there is a problem with supervisor
// fallback to observer logs if there is a problem with supervisor
this._loadObserverLogs();
}
}
@@ -251,6 +253,9 @@ class LandingPageLogs extends LitElement {
this._scheduleObserverLogs();
} catch (err) {
// wait because there is a moment where landingpage is down and core is not up yet
await waitForSeconds(ASSUME_CORE_START_SECONDS);
// eslint-disable-next-line no-console
console.error(err);
this._error = true;

View File

@@ -1,13 +1,7 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import {
type CSSResultGroup,
LitElement,
type PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type {
LandingPageKeys,
LocalizeFunc,
@@ -16,33 +10,24 @@ import "../../../src/components/ha-button";
import "../../../src/components/ha-alert";
import {
ALTERNATIVE_DNS_SERVERS,
getSupervisorNetworkInfo,
setSupervisorNetworkDns,
type NetworkInfo,
} from "../data/supervisor";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
import type { NetworkInterface } from "../../../src/data/hassio/network";
import { fireEvent } from "../../../src/common/dom/fire_event";
@customElement("landing-page-network")
class LandingPageNetwork extends LitElement {
@property({ attribute: false })
public localize!: LocalizeFunc<LandingPageKeys>;
@state() private _networkIssue = false;
@property({ attribute: false }) public networkInfo?: NetworkInfo;
@state() private _getNetworkInfoError = false;
@state() private _dnsPrimaryInterfaceNameservers?: string;
@state() private _dnsPrimaryInterface?: string;
@property({ type: Boolean }) public error = false;
protected render() {
if (!this._networkIssue && !this._getNetworkInfoError) {
return nothing;
}
if (this._getNetworkInfoError) {
if (this.error) {
return html`
<ha-alert alert-type="error">
<p>${this.localize("network_issue.error_get_network_info")}</p>
@@ -50,6 +35,16 @@ class LandingPageNetwork extends LitElement {
`;
}
let dnsPrimaryInterfaceNameservers: string | undefined;
const primaryInterface = this._getPrimaryInterface(
this.networkInfo?.interfaces
);
if (primaryInterface) {
dnsPrimaryInterfaceNameservers =
this._getPrimaryNameservers(primaryInterface);
}
return html`
<ha-alert
alert-type="warning"
@@ -57,11 +52,11 @@ class LandingPageNetwork extends LitElement {
>
<p>
${this.localize("network_issue.description", {
dns: this._dnsPrimaryInterfaceNameservers || "?",
dns: dnsPrimaryInterfaceNameservers || "?",
})}
</p>
<p>${this.localize("network_issue.resolve_different")}</p>
${!this._dnsPrimaryInterfaceNameservers
${!dnsPrimaryInterfaceNameservers
? html`
<p>
<b>${this.localize("network_issue.no_primary_interface")} </b>
@@ -73,7 +68,7 @@ class LandingPageNetwork extends LitElement {
({ translationKey }, key) =>
html`<ha-button
.index=${key}
.disabled=${!this._dnsPrimaryInterfaceNameservers}
.disabled=${!dnsPrimaryInterfaceNameservers}
@click=${this._setDns}
>${this.localize(translationKey)}</ha-button
>`
@@ -83,76 +78,40 @@ class LandingPageNetwork extends LitElement {
`;
}
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
this._fetchSupervisorInfo();
}
private _getPrimaryInterface = memoizeOne((interfaces?: NetworkInterface[]) =>
interfaces?.find((intf) => intf.primary && intf.enabled)
);
private _scheduleFetchSupervisorInfo() {
setTimeout(
() => this._fetchSupervisorInfo(),
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
);
}
private async _fetchSupervisorInfo() {
let data;
try {
const response = await getSupervisorNetworkInfo();
if (!response.ok) {
throw new Error("Failed to fetch network info");
}
({ data } = await response.json());
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
this._getNetworkInfoError = true;
this._dnsPrimaryInterfaceNameservers = undefined;
this._dnsPrimaryInterface = undefined;
return;
}
this._getNetworkInfoError = false;
const primaryInterface = data.interfaces.find(
(intf) => intf.primary && intf.enabled
);
if (primaryInterface) {
this._dnsPrimaryInterfaceNameservers = [
private _getPrimaryNameservers = memoizeOne(
(primaryInterface: NetworkInterface) =>
[
...(primaryInterface.ipv4?.nameservers || []),
...(primaryInterface.ipv6?.nameservers || []),
].join(", ");
this._dnsPrimaryInterface = primaryInterface.interface;
} else {
this._dnsPrimaryInterfaceNameservers = undefined;
this._dnsPrimaryInterface = undefined;
}
if (!data.host_internet) {
this._networkIssue = true;
} else {
this._networkIssue = false;
}
fireEvent(this, "value-changed", {
value: this._networkIssue,
});
this._scheduleFetchSupervisorInfo();
}
].join(", ")
);
private async _setDns(ev) {
const primaryInterface = this._getPrimaryInterface(
this.networkInfo?.interfaces
);
const index = ev.target?.index;
try {
const dnsPrimaryInterface = primaryInterface?.interface;
if (!dnsPrimaryInterface) {
throw new Error("No primary interface found");
}
const response = await setSupervisorNetworkDns(
index,
this._dnsPrimaryInterface!
dnsPrimaryInterface
);
if (!response.ok) {
throw new Error("Failed to set DNS");
}
this._networkIssue = false;
// notify landing page to trigger a network info reload
fireEvent(this, "dns-set");
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
@@ -183,4 +142,7 @@ declare global {
interface HTMLElementTagNameMap {
"landing-page-network": LandingPageNetwork;
}
interface HASSDomEvents {
"dns-set": undefined;
}
}

View File

@@ -1,4 +1,17 @@
import type { LandingPageKeys } from "../../../src/common/translations/localize";
import type { HassioResponse } from "../../../src/data/hassio/common";
import type {
DockerNetwork,
NetworkInterface,
} from "../../../src/data/hassio/network";
import { handleFetchPromise } from "../../../src/util/hass-call-api";
export interface NetworkInfo {
interfaces: NetworkInterface[];
docker: DockerNetwork;
host_internet: boolean;
supervisor_internet: boolean;
}
export const ALTERNATIVE_DNS_SERVERS: {
ipv4: string[];
@@ -18,7 +31,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 +39,29 @@ 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 getSupervisorNetworkInfo() {
return fetch("/supervisor/network/info");
export async function pingSupervisor() {
return fetch("/supervisor-api/supervisor/ping");
}
export async function getSupervisorNetworkInfo(): Promise<NetworkInfo> {
const responseData = await handleFetchPromise<HassioResponse<NetworkInfo>>(
fetch("/supervisor-api/network/info")
);
return responseData?.data;
}
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: {

View File

@@ -10,36 +10,56 @@ import { extractSearchParam } from "../../src/common/url/search-params";
import { onBoardingStyles } from "../../src/onboarding/styles";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { LandingPageBaseElement } from "./landing-page-base-element";
import {
getSupervisorNetworkInfo,
pingSupervisor,
type NetworkInfo,
} from "./data/supervisor";
const SCHEDULE_CORE_CHECK_SECONDS = 5;
export const ASSUME_CORE_START_SECONDS = 60;
const SCHEDULE_CORE_CHECK_SECONDS = 1;
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
@customElement("ha-landing-page")
class HaLandingPage extends LandingPageBaseElement {
@property({ attribute: false }) public translationFragment = "landing-page";
@state() private _networkIssue = false;
@state() private _supervisorError = false;
@state() private _networkInfo?: NetworkInfo;
@state() private _coreStatusChecked = false;
@state() private _networkInfoError = false;
@state() private _coreCheckActive = false;
private _mobileApp =
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
render() {
const networkIssue = this._networkInfo && !this._networkInfo.host_internet;
return html`
<ha-card>
<div class="card-content">
<h1>${this.localize("header")}</h1>
${!this._networkIssue && !this._supervisorError
${!networkIssue && !this._supervisorError
? html`
<p>${this.localize("subheader")}</p>
<mwc-linear-progress indeterminate></mwc-linear-progress>
`
: nothing}
<landing-page-network
@value-changed=${this._networkInfoChanged}
.localize=${this.localize}
></landing-page-network>
${networkIssue || this._networkInfoError
? html`
<landing-page-network
.localize=${this.localize}
.networkInfo=${this._networkInfo}
.error=${this._networkInfoError}
@dns-set=${this._fetchSupervisorInfo}
></landing-page-network>
`
: nothing}
${this._supervisorError
? html`
<ha-alert
@@ -88,24 +108,66 @@ class HaLandingPage extends LandingPageBaseElement {
}
import("../../src/components/ha-language-picker");
this._scheduleCoreCheck();
this._fetchSupervisorInfo(true);
}
private _scheduleCoreCheck() {
private _scheduleFetchSupervisorInfo() {
setTimeout(
() => this._checkCoreAvailability(),
SCHEDULE_CORE_CHECK_SECONDS * 1000
() => this._fetchSupervisorInfo(true),
// on assumed core start check every second, otherwise every 5 seconds
(this._coreCheckActive
? SCHEDULE_CORE_CHECK_SECONDS
: SCHEDULE_FETCH_NETWORK_INFO_SECONDS) * 1000
);
}
private _scheduleTurnOffCoreCheck() {
setTimeout(() => {
this._coreCheckActive = false;
}, ASSUME_CORE_START_SECONDS * 1000);
}
private async _fetchSupervisorInfo(schedule = false) {
try {
const response = await pingSupervisor();
if (!response.ok) {
throw new Error("ping-failed");
}
this._networkInfo = await getSupervisorNetworkInfo();
this._networkInfoError = false;
this._coreStatusChecked = false;
} catch (err: any) {
if (!this._coreStatusChecked) {
// wait before show errors, because we assume that core is starting
this._coreCheckActive = true;
this._scheduleTurnOffCoreCheck();
}
await this._checkCoreAvailability();
// assume supervisor update if ping fails -> don't show an error
if (!this._coreCheckActive && err.message !== "ping-failed") {
// eslint-disable-next-line no-console
console.error(err);
this._networkInfoError = true;
}
}
if (schedule) {
this._scheduleFetchSupervisorInfo();
}
}
private async _checkCoreAvailability() {
try {
const response = await fetch("/manifest.json");
if (response.ok) {
location.reload();
} else {
throw new Error("Failed to fetch manifest");
}
} finally {
this._scheduleCoreCheck();
} catch (_err) {
this._coreStatusChecked = true;
}
}
@@ -113,10 +175,6 @@ class HaLandingPage extends LandingPageBaseElement {
this._supervisorError = true;
}
private _networkInfoChanged(ev: CustomEvent) {
this._networkIssue = ev.detail.value;
}
private _languageChanged(ev: CustomEvent) {
const language = ev.detail.value;
if (language !== this.language && language) {

View File

@@ -26,25 +26,25 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.26.7",
"@babel/runtime": "7.26.10",
"@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.10",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.4",
"@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,10 +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",
"@lrnwebcomponents/simple-tooltip": "8.0.2",
"@lit-labs/motion": "1.0.8",
"@lit-labs/observers": "2.0.5",
"@lit-labs/virtualizer": "2.1.0",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
@@ -90,18 +89,19 @@
"@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.6",
"@vaadin/vaadin-themable-mixin": "24.6.6",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "2.3.1",
"barcode-detector": "3.0.1",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.40.0",
"core-js": "3.41.0",
"cropperjs": "1.6.2",
"date-fns": "4.1.0",
"date-fns-tz": "3.2.0",
@@ -109,23 +109,25 @@
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"echarts": "5.6.0",
"element-internals-polyfill": "1.3.13",
"fuse.js": "7.0.0",
"element-internals-polyfill": "3.0.1",
"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 +139,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 +154,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",
"@bundle-stats/plugin-webpack-filter": "4.19.0",
"@lokalise/node-api": "14.0.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.7",
"@rspack/core": "1.2.7",
"@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 +177,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,22 +186,20 @@
"@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",
"babel-loader": "9.2.1",
"@vitest/coverage-v8": "3.0.8",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.19.0",
"eslint": "9.22.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.0.1",
"eslint-config-prettier": "10.1.1",
"eslint-import-resolver-webpack": "0.13.10",
"eslint-plugin-import": "2.31.0",
"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 +216,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.3",
"rspack-manifest-plugin": "5.0.3",
"serve": "14.2.4",
"sinon": "19.0.2",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.11",
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.7.3",
"vitest": "3.0.4",
"typescript": "5.8.2",
"typescript-eslint": "8.26.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.0.8",
"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,8 +241,8 @@
"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": "yarn@4.6.0"
"packageManager": "yarn@4.7.0"
}

View File

@@ -0,0 +1,10 @@
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="64" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="63" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="black" fill-opacity="0.12"/>
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="black" fill-opacity="0.12"/>
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,10 @@
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V56C94 60.4183 90.4183 64 86 64H8C3.58172 64 0 60.4183 0 56V8Z" fill="#1C1C1C"/>
<path d="M0.5 8C0.5 3.85786 3.85786 0.5 8 0.5H86C90.1421 0.5 93.5 3.85786 93.5 8V56C93.5 60.1421 90.1421 63.5 86 63.5H8C3.85786 63.5 0.5 60.1421 0.5 56V8Z" stroke="white" stroke-opacity="0.24"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.48"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="white" fill-opacity="0.24"/>
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="white" fill-opacity="0.24"/>
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="white" fill-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,7 @@
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="black" fill-opacity="0.12"/>
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="black" fill-opacity="0.12"/>
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="black" fill-opacity="0.12"/>
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="black" fill-opacity="0.12"/>
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 964 B

View File

@@ -0,0 +1,7 @@
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="white" fill-opacity="0.24"/>
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="white" fill-opacity="0.24"/>
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="white" fill-opacity="0.24"/>
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="white" fill-opacity="0.24"/>
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="white" fill-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 964 B

View File

@@ -0,0 +1,7 @@
<svg width="94" height="40" viewBox="0 0 94 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="40" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="39" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<circle cx="20" cy="20" r="12" fill="black" fill-opacity="0.12"/>
<path d="M40 14C40 10.6863 42.6863 8 46 8H65C68.3137 8 71 10.6863 71 14C71 17.3137 68.3137 20 65 20H46C42.6863 20 40 17.3137 40 14Z" fill="black" fill-opacity="0.32"/>
<path d="M40 28C40 25.7909 41.7909 24 44 24H77C79.2091 24 81 25.7909 81 28C81 30.2091 79.2091 32 77 32H44C41.7909 32 40 30.2091 40 28Z" fill="black" fill-opacity="0.32"/>
</svg>

After

Width:  |  Height:  |  Size: 652 B

View File

@@ -0,0 +1,7 @@
<svg width="94" height="40" viewBox="0 0 94 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="40" rx="8" fill="#1C1C1C"/>
<rect x="0.5" y="0.5" width="93" height="39" rx="7.5" stroke="white" stroke-opacity="0.24"/>
<circle cx="20" cy="20" r="12" fill="white" fill-opacity="0.24"/>
<path d="M40 14C40 10.6863 42.6863 8 46 8H65C68.3137 8 71 10.6863 71 14C71 17.3137 68.3137 20 65 20H46C42.6863 20 40 17.3137 40 14Z" fill="white" fill-opacity="0.48"/>
<path d="M40 28C40 25.7909 41.7909 24 44 24H77C79.2091 24 81 25.7909 81 28C81 30.2091 79.2091 32 77 32H44C41.7909 32 40 30.2091 40 28Z" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 654 B

View File

@@ -0,0 +1,7 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="72" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="71" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<circle cx="47" cy="20" r="12" fill="black" fill-opacity="0.12"/>
<path d="M31.5 46C31.5 42.6863 34.1863 40 37.5 40H56.5C59.8137 40 62.5 42.6863 62.5 46C62.5 49.3137 59.8137 52 56.5 52H37.5C34.1863 52 31.5 49.3137 31.5 46Z" fill="black" fill-opacity="0.32"/>
<path d="M26.5 60C26.5 57.7909 28.2909 56 30.5 56H63.5C65.7091 56 67.5 57.7909 67.5 60C67.5 62.2091 65.7091 64 63.5 64H30.5C28.2909 64 26.5 62.2091 26.5 60Z" fill="black" fill-opacity="0.32"/>
</svg>

After

Width:  |  Height:  |  Size: 699 B

View File

@@ -0,0 +1,7 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="72" rx="8" fill="#1C1C1C"/>
<rect x="0.5" y="0.5" width="93" height="71" rx="7.5" stroke="white" stroke-opacity="0.24"/>
<circle cx="47" cy="20" r="12" fill="white" fill-opacity="0.24"/>
<path d="M31.5 46C31.5 42.6863 34.1863 40 37.5 40H56.5C59.8137 40 62.5 42.6863 62.5 46C62.5 49.3137 59.8137 52 56.5 52H37.5C34.1863 52 31.5 49.3137 31.5 46Z" fill="white" fill-opacity="0.48"/>
<path d="M26.5 60C26.5 57.7909 28.2909 56 30.5 56H63.5C65.7091 56 67.5 57.7909 67.5 60C67.5 62.2091 65.7091 64 63.5 64H30.5C28.2909 64 26.5 62.2091 26.5 60Z" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

View File

@@ -0,0 +1,6 @@
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="48" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="47" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<rect x="8" y="8" width="78" height="12" rx="3" fill="black" fill-opacity="0.12"/>
<rect x="8" y="28" width="78" height="12" rx="3" fill="black" fill-opacity="0.32"/>
</svg>

After

Width:  |  Height:  |  Size: 414 B

View File

@@ -0,0 +1,6 @@
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="48" rx="8" fill="#1C1C1C"/>
<rect x="0.5" y="0.5" width="93" height="47" rx="7.5" stroke="white" stroke-opacity="0.24"/>
<rect x="8" y="8" width="78" height="12" rx="3" fill="white" fill-opacity="0.24"/>
<rect x="8" y="28" width="78" height="12" rx="3" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -0,0 +1,6 @@
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="28" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<rect x="8" y="8" width="35" height="12" rx="3" fill="black" fill-opacity="0.12"/>
<rect x="51" y="8" width="35" height="12" rx="3" fill="black" fill-opacity="0.32"/>
</svg>

After

Width:  |  Height:  |  Size: 414 B

View File

@@ -0,0 +1,6 @@
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="28" rx="8" fill="#1C1C1C"/>
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="white" stroke-opacity="0.24"/>
<rect x="8" y="8" width="35" height="12" rx="3" fill="white" fill-opacity="0.24"/>
<rect x="51" y="8" width="35" height="12" rx="3" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="56" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.12"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M8 44C8 46.2091 9.79086 48 12 48H16C18.2091 48 20 46.2091 20 44C20 41.7909 18.2091 40 16 40H12C9.79086 40 8 41.7909 8 44Z" fill="black" fill-opacity="0.32"/>
<path d="M24.5 44C24.5 46.2091 26.2909 48 28.5 48H32.5C34.7091 48 36.5 46.2091 36.5 44C36.5 41.7909 34.7091 40 32.5 40H28.5C26.2909 40 24.5 41.7909 24.5 44Z" fill="black" fill-opacity="0.32"/>
<path d="M41 44C41 46.2091 42.7909 48 45 48H49C51.2091 48 53 46.2091 53 44C53 41.7909 51.2091 40 49 40H45C42.7909 40 41 41.7909 41 44Z" fill="black" fill-opacity="0.32"/>
<path d="M57.5 44C57.5 46.2091 59.2909 48 61.5 48H65.5C67.7091 48 69.5 46.2091 69.5 44C69.5 41.7909 67.7091 40 65.5 40H61.5C59.2909 40 57.5 41.7909 57.5 44Z" fill="black" fill-opacity="0.32"/>
<path d="M74 44C74 46.2091 75.7909 48 78 48H82C84.2091 48 86 46.2091 86 44C86 41.7909 84.2091 40 82 40H78C75.7909 40 74 41.7909 74 44Z" fill="black" fill-opacity="0.32"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.24"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M8 44C8 46.2091 9.79086 48 12 48H16C18.2091 48 20 46.2091 20 44C20 41.7909 18.2091 40 16 40H12C9.79086 40 8 41.7909 8 44Z" fill="white" fill-opacity="0.48"/>
<path d="M24.5 44C24.5 46.2091 26.2909 48 28.5 48H32.5C34.7091 48 36.5 46.2091 36.5 44C36.5 41.7909 34.7091 40 32.5 40H28.5C26.2909 40 24.5 41.7909 24.5 44Z" fill="white" fill-opacity="0.48"/>
<path d="M41 44C41 46.2091 42.7909 48 45 48H49C51.2091 48 53 46.2091 53 44C53 41.7909 51.2091 40 49 40H45C42.7909 40 41 41.7909 41 44Z" fill="white" fill-opacity="0.48"/>
<path d="M57.5 44C57.5 46.2091 59.2909 48 61.5 48H65.5C67.7091 48 69.5 46.2091 69.5 44C69.5 41.7909 67.7091 40 65.5 40H61.5C59.2909 40 57.5 41.7909 57.5 44Z" fill="white" fill-opacity="0.48"/>
<path d="M74 44C74 46.2091 75.7909 48 78 48H82C84.2091 48 86 46.2091 86 44C86 41.7909 84.2091 40 82 40H78C75.7909 40 74 41.7909 74 44Z" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="56" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M8 12C8 14.2091 9.79086 16 12 16H16C18.2091 16 20 14.2091 20 12C20 9.79086 18.2091 8 16 8H12C9.79086 8 8 9.79086 8 12Z" fill="black" fill-opacity="0.32"/>
<path d="M24.5 12C24.5 14.2091 26.2909 16 28.5 16H32.5C34.7091 16 36.5 14.2091 36.5 12C36.5 9.79086 34.7091 8 32.5 8H28.5C26.2909 8 24.5 9.79086 24.5 12Z" fill="black" fill-opacity="0.32"/>
<path d="M41 12C41 14.2091 42.7909 16 45 16H49C51.2091 16 53 14.2091 53 12C53 9.79086 51.2091 8 49 8H45C42.7909 8 41 9.79086 41 12Z" fill="black" fill-opacity="0.32"/>
<path d="M57.5 12C57.5 14.2091 59.2909 16 61.5 16H65.5C67.7091 16 69.5 14.2091 69.5 12C69.5 9.79086 67.7091 8 65.5 8H61.5C59.2909 8 57.5 9.79086 57.5 12Z" fill="black" fill-opacity="0.32"/>
<path d="M74 12C74 14.2091 75.7909 16 78 16H82C84.2091 16 86 14.2091 86 12C86 9.79086 84.2091 8 82 8H78C75.7909 8 74 9.79086 74 12Z" fill="black" fill-opacity="0.32"/>
<path d="M8 30C8 26.6863 10.6863 24 14 24H33C36.3137 24 39 26.6863 39 30C39 33.3137 36.3137 36 33 36H14C10.6863 36 8 33.3137 8 30Z" fill="black" fill-opacity="0.12"/>
<path d="M8 43C8 41.3431 9.34315 40 11 40H83C84.6569 40 86 41.3431 86 43V45C86 46.6569 84.6569 48 83 48H11C9.34315 48 8 46.6569 8 45V43Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M8 12C8 14.2091 9.79086 16 12 16H16C18.2091 16 20 14.2091 20 12C20 9.79086 18.2091 8 16 8H12C9.79086 8 8 9.79086 8 12Z" fill="white" fill-opacity="0.48"/>
<path d="M24.5 12C24.5 14.2091 26.2909 16 28.5 16H32.5C34.7091 16 36.5 14.2091 36.5 12C36.5 9.79086 34.7091 8 32.5 8H28.5C26.2909 8 24.5 9.79086 24.5 12Z" fill="white" fill-opacity="0.48"/>
<path d="M41 12C41 14.2091 42.7909 16 45 16H49C51.2091 16 53 14.2091 53 12C53 9.79086 51.2091 8 49 8H45C42.7909 8 41 9.79086 41 12Z" fill="white" fill-opacity="0.48"/>
<path d="M57.5 12C57.5 14.2091 59.2909 16 61.5 16H65.5C67.7091 16 69.5 14.2091 69.5 12C69.5 9.79086 67.7091 8 65.5 8H61.5C59.2909 8 57.5 9.79086 57.5 12Z" fill="white" fill-opacity="0.48"/>
<path d="M74 12C74 14.2091 75.7909 16 78 16H82C84.2091 16 86 14.2091 86 12C86 9.79086 84.2091 8 82 8H78C75.7909 8 74 9.79086 74 12Z" fill="white" fill-opacity="0.48"/>
<path d="M8 30C8 26.6863 10.6863 24 14 24H33C36.3137 24 39 26.6863 39 30C39 33.3137 36.3137 36 33 36H14C10.6863 36 8 33.3137 8 30Z" fill="white" fill-opacity="0.24"/>
<path d="M8 43C8 41.3431 9.34315 40 11 40H83C84.6569 40 86 41.3431 86 43V45C86 46.6569 84.6569 48 83 48H11C9.34315 48 8 46.6569 8 45V43Z" fill="white" fill-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="56" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M31.5 14C31.5 10.6863 34.1863 8 37.5 8H56.5C59.8137 8 62.5 10.6863 62.5 14C62.5 17.3137 59.8137 20 56.5 20H37.5C34.1863 20 31.5 17.3137 31.5 14Z" fill="black" fill-opacity="0.32"/>
<path d="M23 27C23 25.3431 24.3431 24 26 24H68C69.6569 24 71 25.3431 71 27V29C71 30.6569 69.6569 32 68 32H26C24.3431 32 23 30.6569 23 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M9 44C9 41.7909 10.7909 40 13 40H17C19.2091 40 21 41.7909 21 44C21 46.2091 19.2091 48 17 48H13C10.7909 48 9 46.2091 9 44Z" fill="black" fill-opacity="0.32"/>
<path d="M25 44C25 41.7909 26.7909 40 29 40H33C35.2091 40 37 41.7909 37 44C37 46.2091 35.2091 48 33 48H29C26.7909 48 25 46.2091 25 44Z" fill="black" fill-opacity="0.12"/>
<path d="M41 44C41 41.7909 42.7909 40 45 40H49C51.2091 40 53 41.7909 53 44C53 46.2091 51.2091 48 49 48H45C42.7909 48 41 46.2091 41 44Z" fill="black" fill-opacity="0.12"/>
<path d="M57 44C57 41.7909 58.7909 40 61 40H65C67.2091 40 69 41.7909 69 44C69 46.2091 67.2091 48 65 48H61C58.7909 48 57 46.2091 57 44Z" fill="black" fill-opacity="0.12"/>
<path d="M73 44C73 41.7909 74.7909 40 77 40H81C83.2091 40 85 41.7909 85 44C85 46.2091 83.2091 48 81 48H77C74.7909 48 73 46.2091 73 44Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M31.5 14C31.5 10.6863 34.1863 8 37.5 8H56.5C59.8137 8 62.5 10.6863 62.5 14C62.5 17.3137 59.8137 20 56.5 20H37.5C34.1863 20 31.5 17.3137 31.5 14Z" fill="white" fill-opacity="0.48"/>
<path d="M23 27C23 25.3431 24.3431 24 26 24H68C69.6569 24 71 25.3431 71 27V29C71 30.6569 69.6569 32 68 32H26C24.3431 32 23 30.6569 23 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M9 44C9 41.7909 10.7909 40 13 40H17C19.2091 40 21 41.7909 21 44C21 46.2091 19.2091 48 17 48H13C10.7909 48 9 46.2091 9 44Z" fill="white" fill-opacity="0.48"/>
<rect x="25" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="41" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="57" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="73" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,24 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_887_2968)">
<path d="M0 39H55V64C55 68.4183 51.4183 72 47 72H8C3.58172 72 0 68.4183 0 64V39Z" fill="white"/>
<path d="M1.34748 68.4449C0.772837 67.5866 0.359906 66.6109 0.152272 65.5613L0.642766 65.4643C0.549158 64.9911 0.5 64.5015 0.5 64V61.9167H0V57.75H0.5V53.5833H0V49.4167H0.5V45.25H0V41.0833H0.5V39.5H1.96429V39H5.89286V39.5H9.82143V39H13.75V39.5H17.6786V39H21.6071V39.5H25.5357V39H29.4643V39.5H33.3929V39H37.3214V39.5H41.25V39H45.1786V39.5H49.1071V39H53.0357V39.5H54.5V41.0833H55V45.25H54.5V49.4167H55V53.5833H54.5V57.75H55V61.9167H54.5V64C54.5 64.5015 54.4508 64.9911 54.3572 65.4643L54.8477 65.5613C54.6401 66.6109 54.2272 67.5866 53.6525 68.4449L53.237 68.1668C52.6893 68.9849 51.9849 69.6893 51.1668 70.237L51.4449 70.6525C50.5866 71.2272 49.6109 71.6401 48.5613 71.8477L48.4643 71.3572C47.9911 71.4508 47.5015 71.5 47 71.5H45.05V72H41.15V71.5H37.25V72H33.35V71.5H29.45V72H25.55V71.5H21.65V72H17.75V71.5H13.85V72H9.95V71.5H8C7.49847 71.5 7.00892 71.4508 6.53574 71.3572L6.4387 71.8477C5.38915 71.6401 4.41341 71.2272 3.55507 70.6525L3.83323 70.237C3.01513 69.6893 2.31067 68.9849 1.76296 68.1668L1.34748 68.4449Z" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<rect x="8" y="47" width="12" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<rect x="24" y="47" width="12" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<rect x="8" y="59" width="12" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M54 0H86C90.4183 0 94 3.58172 94 8V32C94 36.4183 90.4183 40 86 40H54V0Z" fill="white"/>
<path d="M84 39.5V40H80V39.5H76V40H72V39.5H68V40H64V39.5H60V40H56V39.5H54.5V38H54V34H54.5V30H54V26H54.5V22H54V18H54.5V14H54V10H54.5V6H54V2H54.5V0.5H56V0H60V0.5H64V0H68V0.5H72V0H76V0.5H80V0H84V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152272C88.6109 0.359906 89.5866 0.772836 90.4449 1.34748L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V32C93.5 32.5015 93.4508 32.9911 93.3572 33.4643L93.8477 33.5613C93.6401 34.6109 93.2272 35.5866 92.6525 36.4449L92.237 36.1668C91.6893 36.9849 90.9849 37.6893 90.1668 38.237L90.4449 38.6525C89.5866 39.2272 88.6109 39.6401 87.5613 39.8477L87.4643 39.3572C86.9911 39.4508 86.5015 39.5 86 39.5H84Z" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M58 28C58 30.2091 59.7909 32 62 32H66C68.2091 32 70 30.2091 70 28C70 25.7909 68.2091 24 66 24H62C59.7909 24 58 25.7909 58 28Z" fill="black" fill-opacity="0.12"/>
<path d="M74 28C74 30.2091 75.7909 32 78 32H82C84.2091 32 86 30.2091 86 28C86 25.7909 84.2091 24 82 24H78C75.7909 24 74 25.7909 74 28Z" fill="black" fill-opacity="0.12"/>
<path d="M74 16C74 18.2091 75.7909 20 78 20H82C84.2091 20 86 18.2091 86 16C86 13.7909 84.2091 12 82 12H78C75.7909 12 74 13.7909 74 16Z" fill="black" fill-opacity="0.32"/>
<path d="M0 8C0 3.58172 3.58172 0 8 0H55V40H0V8Z" fill="white"/>
<path d="M3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.95833V0H13.875V0.5H17.7917V0H21.7083V0.5H25.625V0H29.5417V0.5H33.4583V0H37.375V0.5H41.2917V0H45.2083V0.5H49.125V0H53.0417V0.5H54.5V2H55V6H54.5V10H55V14H54.5V18H55V22H54.5V26H55V30H54.5V34H55V38H54.5V39.5H53.0357V40H49.1071V39.5H45.1786V40H41.25V39.5H37.3214V40H33.3929V39.5H29.4643V40H25.5357V39.5H21.6071V40H17.6786V39.5H13.75V40H9.82143V39.5H5.89286V40H1.96429V39.5H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748Z" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H44C45.6569 24 47 25.3431 47 27V29C47 30.6569 45.6569 32 44 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M79 48V54.5C79 58.09 76.09 61 72.5 61H66.83L69.92 64.09L68.5 65.5L63 60L68.5 54.5L69.91 55.91L66.83 59H72.5C75 59 77 57 77 54.5V48H79Z" fill="black" fill-opacity="0.32"/>
</g>
<defs>
<clipPath id="clip0_887_2968">
<rect width="94" height="72" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,17 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 39H55V64C55 68.4183 51.4183 72 47 72H8C3.58172 72 0 68.4183 0 64V39Z" fill="#1C1C1C"/>
<path d="M1.34748 68.4449C0.772837 67.5866 0.359906 66.6109 0.152272 65.5613L0.642766 65.4643C0.549158 64.9911 0.5 64.5015 0.5 64V61.9167H0V57.75H0.5V53.5833H0V49.4167H0.5V45.25H0V41.0833H0.5V39.5H1.96429V39H5.89286V39.5H9.82143V39H13.75V39.5H17.6786V39H21.6071V39.5H25.5357V39H29.4643V39.5H33.3929V39H37.3214V39.5H41.25V39H45.1786V39.5H49.1071V39H53.0357V39.5H54.5V41.0833H55V45.25H54.5V49.4167H55V53.5833H54.5V57.75H55V61.9167H54.5V64C54.5 64.5015 54.4508 64.9911 54.3572 65.4643L54.8477 65.5613C54.6401 66.6109 54.2272 67.5866 53.6525 68.4449L53.237 68.1668C52.6893 68.9849 51.9849 69.6893 51.1668 70.237L51.4449 70.6525C50.5866 71.2272 49.6109 71.6401 48.5613 71.8477L48.4643 71.3572C47.9911 71.4508 47.5015 71.5 47 71.5H45.05V72H41.15V71.5H37.25V72H33.35V71.5H29.45V72H25.55V71.5H21.65V72H17.75V71.5H13.85V72H9.95V71.5H8C7.49847 71.5 7.00892 71.4508 6.53574 71.3572L6.4387 71.8477C5.38915 71.6401 4.41341 71.2272 3.55507 70.6525L3.83323 70.237C3.01513 69.6893 2.31067 68.9849 1.76296 68.1668L1.34748 68.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M8 51C8 48.7909 9.79086 47 12 47H16C18.2091 47 20 48.7909 20 51C20 53.2091 18.2091 55 16 55H12C9.79086 55 8 53.2091 8 51Z" fill="white" fill-opacity="0.48"/>
<path d="M24 51C24 48.7909 25.7909 47 28 47H32C34.2091 47 36 48.7909 36 51C36 53.2091 34.2091 55 32 55H28C25.7909 55 24 53.2091 24 51Z" fill="white" fill-opacity="0.24"/>
<path d="M8 63C8 60.7909 9.79086 59 12 59H16C18.2091 59 20 60.7909 20 63C20 65.2091 18.2091 67 16 67H12C9.79086 67 8 65.2091 8 63Z" fill="white" fill-opacity="0.24"/>
<path d="M54 0H86C90.4183 0 94 3.58172 94 8V32C94 36.4183 90.4183 40 86 40H54V0Z" fill="#1C1C1C"/>
<path d="M84 39.5V40H80V39.5H76V40H72V39.5H68V40H64V39.5H60V40H56V39.5H54.5V38H54V34H54.5V30H54V26H54.5V22H54V18H54.5V14H54V10H54.5V6H54V2H54.5V0.5H56V0H60V0.5H64V0H68V0.5H72V0H76V0.5H80V0H84V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152272C88.6109 0.359906 89.5866 0.772836 90.4449 1.34748L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V32C93.5 32.5015 93.4508 32.9911 93.3572 33.4643L93.8477 33.5613C93.6401 34.6109 93.2272 35.5866 92.6525 36.4449L92.237 36.1668C91.6893 36.9849 90.9849 37.6893 90.1668 38.237L90.4449 38.6525C89.5866 39.2272 88.6109 39.6401 87.5613 39.8477L87.4643 39.3572C86.9911 39.4508 86.5015 39.5 86 39.5H84Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M58 28C58 30.2091 59.7909 32 62 32H66C68.2091 32 70 30.2091 70 28C70 25.7909 68.2091 24 66 24H62C59.7909 24 58 25.7909 58 28Z" fill="white" fill-opacity="0.24"/>
<path d="M74 28C74 30.2091 75.7909 32 78 32H82C84.2091 32 86 30.2091 86 28C86 25.7909 84.2091 24 82 24H78C75.7909 24 74 25.7909 74 28Z" fill="white" fill-opacity="0.24"/>
<path d="M74 16C74 18.2091 75.7909 20 78 20H82C84.2091 20 86 18.2091 86 16C86 13.7909 84.2091 12 82 12H78C75.7909 12 74 13.7909 74 16Z" fill="white" fill-opacity="0.48"/>
<path d="M0 8C0 3.58172 3.58172 0 8 0H55V40H0V8Z" fill="#1C1C1C"/>
<path d="M3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.95833V0H13.875V0.5H17.7917V0H21.7083V0.5H25.625V0H29.5417V0.5H33.4583V0H37.375V0.5H41.2917V0H45.2083V0.5H49.125V0H53.0417V0.5H54.5V2H55V6H54.5V10H55V14H54.5V18H55V22H54.5V26H55V30H54.5V34H55V38H54.5V39.5H53.0357V40H49.1071V39.5H45.1786V40H41.25V39.5H37.3214V40H33.3929V39.5H29.4643V40H25.5357V39.5H21.6071V40H17.6786V39.5H13.75V40H9.82143V39.5H5.89286V40H1.96429V39.5H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.48"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H44C45.6569 24 47 25.3431 47 27V29C47 30.6569 45.6569 32 44 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M79 48V54.5C79 58.09 76.09 61 72.5 61H66.83L69.92 64.09L68.5 65.5L63 60L68.5 54.5L69.91 55.91L66.83 59H72.5C75 59 77 57 77 54.5V48H79Z" fill="white" fill-opacity="0.48"/>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -0,0 +1,9 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="94" height="56" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H53C54.6569 24 56 25.3431 56 27V29C56 30.6569 54.6569 32 53 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
<path d="M8 44C8 41.7909 9.79086 40 12 40H16C18.2091 40 20 41.7909 20 44C20 46.2091 18.2091 48 16 48H12C9.79086 48 8 46.2091 8 44Z" fill="black" fill-opacity="0.32"/>
<path d="M24 44C24 41.7909 25.7909 40 28 40H32C34.2091 40 36 41.7909 36 44C36 46.2091 34.2091 48 32 48H28C25.7909 48 24 46.2091 24 44Z" fill="black" fill-opacity="0.12"/>
<path d="M40 44C40 41.7909 41.7909 40 44 40H48C50.2091 40 52 41.7909 52 44C52 46.2091 50.2091 48 48 48H44C41.7909 48 40 46.2091 40 44Z" fill="black" fill-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,11 @@
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.48"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H53C54.6569 24 56 25.3431 56 27V29C56 30.6569 54.6569 32 53 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M8 44C8 41.7909 9.79086 40 12 40H16C18.2091 40 20 41.7909 20 44C20 46.2091 18.2091 48 16 48H12C9.79086 48 8 46.2091 8 44Z" fill="white" fill-opacity="0.48"/>
<rect x="24" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="40" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="56" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
<rect x="72" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

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

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
script/serve-config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"cleanUrls": false
}

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")
);

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;
}

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,

View File

@@ -16,11 +16,22 @@ 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);
map.setView([52.3731339, 4.8903147], 13);
const tileLayer = createTileLayer(Leaflet).addTo(map);

View File

@@ -1,2 +1,2 @@
export const computeDomain = (entityId: string): string =>
entityId.substr(0, entityId.indexOf("."));
entityId.substring(0, entityId.indexOf("."));

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")
) {

View File

@@ -0,0 +1,43 @@
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../../../data/entity_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types";
interface EntityContext {
entity: EntityRegistryDisplayEntry | null;
device: DeviceRegistryEntry | null;
area: AreaRegistryEntry | null;
floor: FloorRegistryEntry | null;
}
export const getEntityContext = (
entityId: string,
hass: HomeAssistant
): EntityContext => {
const entity =
(hass.entities[entityId] as EntityRegistryDisplayEntry | undefined) || null;
if (!entity) {
return {
entity: null,
device: null,
area: null,
floor: null,
};
}
const deviceId = entity?.device_id;
const device = deviceId ? hass.devices[deviceId] : null;
const areaId = entity?.area_id || device?.area_id;
const area = areaId ? hass.areas[areaId] : null;
const floorId = area?.floor_id;
const floor = floorId ? hass.floors[floorId] : null;
return {
entity: entity,
device: device,
area: area,
floor: floor,
};
};

View File

@@ -0,0 +1,78 @@
import { computeDomain } from "./compute_domain";
export type EntityDomainFilterFunc = (entityId: string) => boolean;
export interface EntityDomainFilter {
include_domains: string[];
include_entities: string[];
exclude_domains: string[];
exclude_entities: string[];
}
export const isEmptyEntityDomainFilter = (filter: EntityDomainFilter) =>
filter.include_domains.length +
filter.include_entities.length +
filter.exclude_domains.length +
filter.exclude_entities.length ===
0;
export const generateEntityDomainFilter = (
includeDomains?: string[],
includeEntities?: string[],
excludeDomains?: string[],
excludeEntities?: string[]
): EntityDomainFilterFunc => {
const includeDomainsSet = new Set(includeDomains);
const includeEntitiesSet = new Set(includeEntities);
const excludeDomainsSet = new Set(excludeDomains);
const excludeEntitiesSet = new Set(excludeEntities);
const haveInclude = includeDomainsSet.size > 0 || includeEntitiesSet.size > 0;
const haveExclude = excludeDomainsSet.size > 0 || excludeEntitiesSet.size > 0;
// Case 1 - no includes or excludes - pass all entities
if (!haveInclude && !haveExclude) {
return () => true;
}
// Case 2 - includes, no excludes - only include specified entities
if (haveInclude && !haveExclude) {
return (entityId) =>
includeEntitiesSet.has(entityId) ||
includeDomainsSet.has(computeDomain(entityId));
}
// Case 3 - excludes, no includes - only exclude specified entities
if (!haveInclude && haveExclude) {
return (entityId) =>
!excludeEntitiesSet.has(entityId) &&
!excludeDomainsSet.has(computeDomain(entityId));
}
// Case 4 - both includes and excludes specified
// Case 4a - include domain specified
// - if domain is included, pass if entity not excluded
// - if domain is not included, pass if entity is included
// note: if both include and exclude domains specified,
// the exclude domains are ignored
if (includeDomainsSet.size) {
return (entityId) =>
includeDomainsSet.has(computeDomain(entityId))
? !excludeEntitiesSet.has(entityId)
: includeEntitiesSet.has(entityId);
}
// Case 4b - exclude domain specified
// - if domain is excluded, pass if entity is included
// - if domain is not excluded, pass if entity not excluded
if (excludeDomainsSet.size) {
return (entityId) =>
excludeDomainsSet.has(computeDomain(entityId))
? includeEntitiesSet.has(entityId)
: !excludeEntitiesSet.has(entityId);
}
// Case 4c - neither include or exclude domain specified
// - Only pass if entity is included. Ignore entity excludes.
return (entityId) => includeEntitiesSet.has(entityId);
};

View File

@@ -1,78 +1,124 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../../types";
import { ensureArray } from "../array/ensure-array";
import { computeDomain } from "./compute_domain";
import { getEntityContext } from "./context/get_entity_context";
export type FilterFunc = (entityId: string) => boolean;
type EntityCategory = "none" | "config" | "diagnostic";
export interface EntityFilter {
include_domains: string[];
include_entities: string[];
exclude_domains: string[];
exclude_entities: string[];
domain?: string | string[];
device_class?: string | string[];
device?: string | string[];
area?: string | string[];
floor?: string | string[];
label?: string | string[];
entity_category?: EntityCategory | EntityCategory[];
hidden_platform?: string | string[];
}
export const isEmptyFilter = (filter: EntityFilter) =>
filter.include_domains.length +
filter.include_entities.length +
filter.exclude_domains.length +
filter.exclude_entities.length ===
0;
type EntityFilterFunc = (entityId: string) => boolean;
export const generateFilter = (
includeDomains?: string[],
includeEntities?: string[],
excludeDomains?: string[],
excludeEntities?: string[]
): FilterFunc => {
const includeDomainsSet = new Set(includeDomains);
const includeEntitiesSet = new Set(includeEntities);
const excludeDomainsSet = new Set(excludeDomains);
const excludeEntitiesSet = new Set(excludeEntities);
export const generateEntityFilter = (
hass: HomeAssistant,
filter: EntityFilter
): EntityFilterFunc => {
const domains = filter.domain
? new Set(ensureArray(filter.domain))
: undefined;
const deviceClasses = filter.device_class
? new Set(ensureArray(filter.device_class))
: undefined;
const floors = filter.floor ? new Set(ensureArray(filter.floor)) : undefined;
const areas = filter.area ? new Set(ensureArray(filter.area)) : undefined;
const devices = filter.device
? new Set(ensureArray(filter.device))
: undefined;
const entityCategories = filter.entity_category
? new Set(ensureArray(filter.entity_category))
: undefined;
const labels = filter.label ? new Set(ensureArray(filter.label)) : undefined;
const hiddenPlatforms = filter.hidden_platform
? new Set(ensureArray(filter.hidden_platform))
: undefined;
const haveInclude = includeDomainsSet.size > 0 || includeEntitiesSet.size > 0;
const haveExclude = excludeDomainsSet.size > 0 || excludeEntitiesSet.size > 0;
return (entityId: string) => {
const stateObj = hass.states[entityId] as HassEntity | undefined;
if (!stateObj) {
return false;
}
if (domains) {
const domain = computeDomain(entityId);
if (!domains.has(domain)) {
return false;
}
}
if (deviceClasses) {
const dc = stateObj.attributes.device_class;
if (!dc) {
return false;
}
if (!deviceClasses.has(dc)) {
return false;
}
}
// Case 1 - no includes or excludes - pass all entities
if (!haveInclude && !haveExclude) {
return () => true;
}
const { area, floor, device, entity } = getEntityContext(entityId, hass);
// Case 2 - includes, no excludes - only include specified entities
if (haveInclude && !haveExclude) {
return (entityId) =>
includeEntitiesSet.has(entityId) ||
includeDomainsSet.has(computeDomain(entityId));
}
if (entity && entity.hidden) {
return false;
}
// Case 3 - excludes, no includes - only exclude specified entities
if (!haveInclude && haveExclude) {
return (entityId) =>
!excludeEntitiesSet.has(entityId) &&
!excludeDomainsSet.has(computeDomain(entityId));
}
if (floors) {
if (!floor) {
return false;
}
if (!floors) {
return false;
}
}
if (areas) {
if (!area) {
return false;
}
if (!areas.has(area.area_id)) {
return false;
}
}
if (devices) {
if (!device) {
return false;
}
if (!devices.has(device.id)) {
return false;
}
}
if (labels) {
if (!entity) {
return false;
}
if (!entity.labels.some((label) => labels.has(label))) {
return false;
}
}
if (entityCategories) {
if (!entity) {
return false;
}
const category = entity?.entity_category || "none";
if (!entityCategories.has(category)) {
return false;
}
}
if (hiddenPlatforms) {
if (!entity) {
return false;
}
if (entity.platform && hiddenPlatforms.has(entity.platform)) {
return false;
}
}
// Case 4 - both includes and excludes specified
// Case 4a - include domain specified
// - if domain is included, pass if entity not excluded
// - if domain is not included, pass if entity is included
// note: if both include and exclude domains specified,
// the exclude domains are ignored
if (includeDomainsSet.size) {
return (entityId) =>
includeDomainsSet.has(computeDomain(entityId))
? !excludeEntitiesSet.has(entityId)
: includeEntitiesSet.has(entityId);
}
// Case 4b - exclude domain specified
// - if domain is excluded, pass if entity is included
// - if domain is not excluded, pass if entity not excluded
if (excludeDomainsSet.size) {
return (entityId) =>
excludeDomainsSet.has(computeDomain(entityId))
? includeEntitiesSet.has(entityId)
: !excludeEntitiesSet.has(entityId);
}
// Case 4c - neither include or exclude domain specified
// - Only pass if entity is included. Ignore entity excludes.
return (entityId) => includeEntitiesSet.has(entityId);
return true;
};
};

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":

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);
}
}

6
src/common/util/wait.ts Normal file
View File

@@ -0,0 +1,6 @@
export const waitForMs = (ms: number) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});
export const waitForSeconds = (seconds: number) => waitForMs(seconds * 1000);

View File

@@ -3,7 +3,7 @@ import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../ha-circular-progress";
import "../ha-spinner";
import "../ha-svg-icon";
@customElement("ha-progress-button")
@@ -35,13 +35,8 @@ export class HaProgressButton extends LitElement {
: this._result === "error"
? html`<ha-svg-icon .path=${mdiAlertOctagram}></ha-svg-icon>`
: this.progress
? html`
<ha-circular-progress
size="small"
indeterminate
></ha-circular-progress>
`
: ""}
? html`<ha-spinner size="small"></ha-spinner>`
: nothing}
</div>
`}
`;
@@ -117,6 +112,9 @@ export class HaProgressButton extends LitElement {
mwc-button.error slot {
visibility: hidden;
}
:host([destructive]) {
--mdc-theme-primary: var(--error-color);
}
`;
}

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);
}

View File

@@ -1,27 +1,37 @@
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 { mdiChevronDown, mdiChevronUp, mdiRestart } from "@mdi/js";
import { differenceInMinutes } from "date-fns";
import type { DataZoomComponentOption } from "echarts/components";
import type { EChartsType } from "echarts/core";
import type {
ECElementEvent,
LegendComponentOption,
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";
import "../chips/ha-assist-chip";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10;
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
const DOUBLE_TAP_TIME = 300;
@customElement("ha-chart-base")
export class HaChartBase extends LitElement {
@@ -35,8 +45,10 @@ export class HaChartBase extends LitElement {
@property({ type: String }) public height?: string;
@property({ attribute: "external-hidden", type: Boolean })
public externalHidden = false;
@property({ attribute: "expand-legend", type: Boolean })
public expandLegend?: boolean;
@property({ attribute: false }) public extraComponents?: any[];
@state()
@consume({ context: themesContext, subscribe: true })
@@ -44,10 +56,18 @@ export class HaChartBase extends LitElement {
@state() private _isZoomed = false;
@state() private _zoomRatio = 1;
@state() private _minutesDifference = 24 * 60;
@state() private _hiddenDatasets = new Set<string>();
private _modifierPressed = false;
private _isTouchDevice = "ontouchstart" in window;
private _lastTapTime?: number;
// @ts-ignore
private _resizeController = new ResizeController(this, {
callback: () => this.chart?.resize(),
@@ -59,12 +79,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,31 +99,46 @@ 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 (
!this._modifierPressed &&
((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() });
}
// drag to zoom
this.chart?.dispatchAction({
type: "takeGlobalCursor",
key: "dataZoomSelect",
dataZoomSelectActive: true,
});
}
};
const handleKeyUp = (ev: KeyboardEvent) => {
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
if (
this._modifierPressed &&
((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() });
}
this.chart?.dispatchAction({
type: "takeGlobalCursor",
key: "dataZoomSelect",
dataZoomSelectActive: false,
});
}
};
@@ -116,49 +155,42 @@ 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;
}
if (changedProps.has("data")) {
this.chart.setOption(
{ series: this.data },
{ lazyUpdate: true, replaceMerge: ["series"] }
);
let chartOptions: ECOption = {};
if (changedProps.has("data") || changedProps.has("_hiddenDatasets")) {
chartOptions.series = this._getSeries();
}
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"
style=${styleMap({
height: this.height ?? `${this._getDefaultHeight()}px`,
})}
@wheel=${this._handleWheel}
class="container ${classMap({ "has-height": !!this.height })}"
style=${styleMap({ height: this.height })}
>
<div class="chart"></div>
<div
class="chart-container"
style=${styleMap({
height: this.height ? undefined : `${this._getDefaultHeight()}px`,
})}
>
<div class="chart"></div>
</div>
${this._renderLegend()}
${this._isZoomed
? html`<ha-icon-button
class="zoom-reset"
@@ -173,6 +205,84 @@ export class HaChartBase extends LitElement {
`;
}
private _renderLegend() {
if (!this.options?.legend || !this.data) {
return nothing;
}
const legend = ensureArray(this.options.legend)[0] as LegendComponentOption;
if (!legend.show) {
return nothing;
}
const datasets = ensureArray(this.data);
const items = (legend.data ||
datasets
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
.map((d) => d.name ?? d.id) ||
[]) as string[];
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
const overflowLimit = isMobile
? LEGEND_OVERFLOW_LIMIT_MOBILE
: LEGEND_OVERFLOW_LIMIT;
return html`<div class="chart-legend">
<ul>
${items.map((item: string, index: number) => {
if (!this.expandLegend && index >= overflowLimit) {
return nothing;
}
const dataset = datasets.find(
(d) => d.id === item || d.name === item
);
const color = dataset?.color as string;
const borderColor = dataset?.itemStyle?.borderColor as string;
return html`<li
.name=${item}
@click=${this._legendClick}
class=${classMap({ hidden: this._hiddenDatasets.has(item) })}
.title=${item}
>
<div
class="bullet"
style=${styleMap({
backgroundColor: color,
borderColor: borderColor || color,
})}
></div>
<div class="label">${item}</div>
</li>`;
})}
${items.length > overflowLimit
? html`<li>
<ha-assist-chip
@click=${this._toggleExpandedLegend}
filled
label=${this.expandLegend
? this.hass.localize(
"ui.components.history_charts.collapse_legend"
)
: `${this.hass.localize("ui.components.history_charts.expand_legend")} (${items.length - overflowLimit})`}
>
<ha-svg-icon
slot="trailing-icon"
.path=${this.expandLegend ? mdiChevronUp : mdiChevronDown}
></ha-svg-icon>
</ha-assist-chip>
</li>`
: nothing}
</ul>
</div>`;
}
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,45 +293,64 @@ export class HaChartBase extends LitElement {
}
const echarts = (await import("../../resources/echarts")).default;
this.chart = echarts.init(
container,
this._themes.darkMode ? "dark" : "light"
);
this.chart.on("legendselectchanged", (params: any) => {
if (this.externalHidden) {
const isSelected = params.selected[params.name];
if (isSelected) {
fireEvent(this, "dataset-unhidden", { name: params.name });
} else {
fireEvent(this, "dataset-hidden", { name: params.name });
}
}
});
if (this.extraComponents?.length) {
echarts.use(this.extraComponents);
}
echarts.registerTheme("custom", this._createTheme());
this.chart = echarts.init(container, "custom");
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);
});
this.chart.on("mousemove", (e: ECElementEvent) => {
if (e.componentType === "series" && e.componentSubType === "custom") {
// custom series do not support cursor style so we need to set it manually
this.chart?.getZr()?.setCursorStyle("default");
this.chart.getZr().on("dblclick", this._handleClickZoom);
if (this._isTouchDevice) {
this.chart.getZr().on("click", (e: ECElementEvent) => {
if (!e.zrByTouch) {
return;
}
if (
this._lastTapTime &&
Date.now() - this._lastTapTime < DOUBLE_TAP_TIME
) {
this._handleClickZoom(e);
} else {
this._lastTapTime = Date.now();
}
});
}
const legend = ensureArray(this.options?.legend || [])[0] as
| LegendComponentOption
| undefined;
Object.entries(legend?.selected || {}).forEach(([stat, selected]) => {
if (selected === false) {
this._hiddenDatasets.add(stat);
}
});
this.chart.setOption({ ...this._createOptions(), series: this.data });
this.chart.setOption({
...this._createOptions(),
series: this._getSeries(),
});
} finally {
this._loading = false;
}
}
private _getDataZoomConfig(): DataZoomComponentOption | undefined {
const xAxis = (this.options?.xAxis?.[0] ??
this.options?.xAxis) as XAXisOption;
const yAxis = (this.options?.yAxis?.[0] ??
this.options?.yAxis) as YAXisOption;
if (xAxis.type === "value" && yAxis.type === "category") {
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
| XAXisOption
| undefined;
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
| YAXisOption
| undefined;
if (xAxis?.type === "value" && yAxis?.type === "category") {
// vertical data zoom doesn't work well in this case and horizontal is pointless
return undefined;
}
@@ -230,31 +359,66 @@ 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,
aria: {
show: true,
},
darkMode: this._themes.darkMode ?? false,
aria: { show: true },
dataZoom: this._getDataZoomConfig(),
toolbox: {
top: Infinity,
left: Infinity,
feature: {
dataZoom: { show: true, yAxisIndex: false, filterMode: "none" },
},
iconStyle: { opacity: 0 },
},
...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,
legend: { show: false },
xAxis,
};
const isMobile = window.matchMedia(
@@ -268,48 +432,267 @@ export class HaChartBase extends LitElement {
tooltips.forEach((tooltip) => {
tooltip.confine = true;
tooltip.appendTo = undefined;
tooltip.triggerOn = "click";
});
options.tooltip = tooltips;
}
return options;
}
private _getDefaultHeight() {
return Math.max(this.clientWidth / 2, 400);
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("--info-color") },
crossStyle: { color: style.getPropertyValue("--info-color") },
},
},
timeline: {},
};
}
private _getSeries() {
if (!Array.isArray(this.data)) {
return this.data;
}
return this.data.filter(
(d) => !this._hiddenDatasets.has(String(d.name ?? d.id))
);
}
private _getDefaultHeight() {
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 _handleClickZoom = (e: ECElementEvent) => {
if (!this.chart) {
return;
}
const range = this._isZoomed
? [0, 100]
: [
(e.offsetX / this.chart.getWidth()) * 100 - 15,
(e.offsetX / this.chart.getWidth()) * 100 + 15,
];
this.chart.dispatchAction({
type: "dataZoom",
start: range[0],
end: range[1],
});
};
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(),
});
}
private _legendClick(ev: any) {
if (!this.chart) {
return;
}
const name = ev.currentTarget?.name;
if (this._hiddenDatasets.has(name)) {
this._hiddenDatasets.delete(name);
fireEvent(this, "dataset-unhidden", { name });
} else {
this._hiddenDatasets.add(name);
fireEvent(this, "dataset-hidden", { name });
}
this.requestUpdate("_hiddenDatasets");
}
private _toggleExpandedLegend() {
this.expandLegend = !this.expandLegend;
setTimeout(() => {
this.chart?.resize();
});
}
static styles = css`
:host {
display: block;
position: relative;
letter-spacing: normal;
}
.container {
display: flex;
flex-direction: column;
position: relative;
}
.container.has-height {
max-height: var(--chart-max-height, 350px);
}
.chart-container {
position: relative;
max-height: var(--chart-max-height, 400px);
width: 100%;
max-height: var(--chart-max-height, 350px);
}
.has-height .chart-container {
flex: 1;
}
.chart {
width: 100%;
height: 100%;
width: 100%;
}
.zoom-reset {
position: absolute;
@@ -321,6 +704,67 @@ export class HaChartBase extends LitElement {
color: var(--primary-color);
border: 1px solid var(--divider-color);
}
.chart-legend {
max-height: 60%;
overflow-y: auto;
padding: 12px 0 0;
font-size: 12px;
color: var(--primary-text-color);
}
.chart-legend ul {
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 8px;
}
.chart-legend li {
height: 24px;
cursor: pointer;
display: inline-flex;
align-items: center;
padding: 0 2px;
box-sizing: border-box;
max-width: 220px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.chart-legend .hidden {
color: var(--secondary-text-color);
}
.chart-legend .label {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.chart-legend .bullet {
border-width: 1px;
border-style: solid;
border-radius: 50%;
display: block;
height: 16px;
width: 16px;
margin-right: 4px;
flex-shrink: 0;
box-sizing: border-box;
margin-inline-end: 4px;
margin-inline-start: initial;
direction: var(--direction);
}
.chart-legend .hidden .bullet {
border-color: var(--secondary-text-color) !important;
background-color: transparent !important;
}
ha-assist-chip {
height: 100%;
--_label-text-weight: 500;
--_leading-space: 8px;
--_trailing-space: 8px;
--_icon-label-space: 4px;
}
`;
}

View File

@@ -1,9 +1,17 @@
import { customElement, property } from "lit/decorators";
import { LitElement, html, css, svg, nothing } from "lit";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { customElement, property, state } from "lit/decorators";
import { LitElement, html, css } from "lit";
import type { EChartsType } from "echarts/core";
import type { CallbackDataParams } from "echarts/types/dist/shared";
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
import { SankeyChart } from "echarts/charts";
import memoizeOne from "memoize-one";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { HomeAssistant } from "../../types";
import type { ECOption } from "../../resources/echarts";
import { measureTextWidth } from "../../util/text";
import "./ha-chart-base";
import { NODE_SIZE } from "../trace/hat-graph-const";
import "../ha-alert";
export interface Node {
id: string;
@@ -25,34 +33,14 @@ export interface SankeyChartData {
links: Link[];
}
type ProcessedNode = Node & {
x: number;
y: number;
size: number;
};
type ProcessedLink = Link & {
value: number;
offset: {
source: number;
target: number;
};
passThroughNodeIds: string[];
};
interface Section {
nodes: ProcessedNode[];
offset: number;
index: number;
totalValue: number;
statePerPixel: number;
}
const MIN_SIZE = 3;
const DEFAULT_COLOR = "var(--primary-color)";
const NODE_WIDTH = 15;
const OVERFLOW_MARGIN = 5;
const FONT_SIZE = 12;
const MIN_DISTANCE = FONT_SIZE / 2;
const NODE_GAP = 8;
const LABEL_DISTANCE = 5;
@customElement("ha-sankey-chart")
export class HaSankeyChart extends LitElement {
@@ -65,141 +53,144 @@ export class HaSankeyChart extends LitElement {
@property({ type: Boolean }) public vertical = false;
@property({ attribute: false }) public loadingText?: string;
@property({ type: String, attribute: false }) public valueFormatter?: (
value: number
) => string;
private _statePerPixel = 0;
public chart?: EChartsType;
private _sizeController = new ResizeController(this, {
@state() private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect,
});
disconnectedCallback() {
super.disconnectedCallback();
}
willUpdate() {
this._statePerPixel = 0;
}
render() {
if (!this._sizeController.value) {
return this.loadingText ?? nothing;
}
const options = {
grid: {
top: 0,
bottom: 0,
left: 0,
right: 0,
},
tooltip: {
trigger: "item",
formatter: this._renderTooltip,
appendTo: document.body,
},
} as ECOption;
const { width, height } = this._sizeController.value;
const { nodes, paths } = this._processNodesAndPaths(
this.data.nodes,
this.data.links
);
return html`
<svg
width=${width}
height=${height}
viewBox="0 0 ${width} ${height}"
preserveAspectRatio="none"
>
<defs>
${paths.map(
(path, i) => svg`
<linearGradient id="gradient${path.sourceNode.id}.${path.targetNode.id}.${i}" gradientTransform="${
this.vertical ? "rotate(90)" : ""
}">
<stop offset="0%" stop-color="${path.sourceNode.color}"></stop>
<stop offset="100%" stop-color="${path.targetNode.color}"></stop>
</linearGradient>
`
)}
</defs>
${paths.map(
(path, i) =>
svg`
<path d="${path.path.map(([cmd, x, y]) => `${cmd}${x},${y}`).join(" ")} Z"
fill="url(#gradient${path.sourceNode.id}.${path.targetNode.id}.${i})" fill-opacity="0.4" />
`
)}
${nodes.map((node) =>
node.passThrough
? nothing
: svg`
<g transform="translate(${node.x},${node.y})">
<rect
class="node"
width=${this.vertical ? node.size : NODE_WIDTH}
height=${this.vertical ? NODE_WIDTH : node.size}
style="fill: ${node.color}"
>
<title>${node.tooltip}</title>
</rect>
${
this.vertical
? nothing
: svg`
<text
class="node-label"
x=${NODE_WIDTH + 5}
y=${node.size / 2}
text-anchor="start"
dominant-baseline="middle"
>${node.label}</text>
`
}
</g>
`
)}
</svg>
${this.vertical
? nodes.map((node) => {
if (!node.label) {
return nothing;
}
const labelWidth = MIN_DISTANCE + node.size;
const fontSize = this._getVerticalLabelFontSize(
node.label,
labelWidth
);
return html`<div
class="node-label vertical"
style="
left: ${node.x - MIN_DISTANCE / 2}px;
top: ${node.y + NODE_WIDTH}px;
width: ${labelWidth}px;
height: ${FONT_SIZE * 3}px;
font-size: ${fontSize}px;
line-height: ${fontSize}px;
"
title=${node.label}
>
${node.label}
</div>`;
})
: nothing}
`;
return html`<ha-chart-base
.data=${this._createData(this.data, this._sizeController.value?.width)}
.options=${options}
height="100%"
.extraComponents=${[SankeyChart]}
></ha-chart-base>`;
}
private _processNodesAndPaths = memoizeOne(
(rawNodes: Node[], rawLinks: Link[]) => {
const filteredNodes = rawNodes.filter((n) => n.value > 0);
const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
const { links, passThroughNodes } = this._processLinks(
filteredNodes,
indexes,
rawLinks
);
const nodes = this._processNodes(
[...filteredNodes, ...passThroughNodes],
indexes
);
const paths = this._processPaths(nodes, links);
return { nodes, paths };
private _renderTooltip = (params: CallbackDataParams) => {
const data = params.data as Record<string, any>;
const value = this.valueFormatter
? this.valueFormatter(data.value)
: data.value;
if (data.id) {
const node = this.data.nodes.find((n) => n.id === data.id);
return `${params.marker} ${node?.label ?? data.id}<br>${value}`;
}
);
if (data.source && data.target) {
const source = this.data.nodes.find((n) => n.id === data.source);
const target = this.data.nodes.find((n) => n.id === data.target);
return `${source?.label ?? data.source}${target?.label ?? data.target}<br>${value}`;
}
return null;
};
private _processLinks(nodes: Node[], indexes: number[], rawLinks: Link[]) {
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
const filteredNodes = data.nodes.filter((n) => n.value > 0);
const indexes = [...new Set(filteredNodes.map((n) => n.index))];
const links = this._processLinks(filteredNodes, data.links);
const sectionWidth = width / indexes.length;
const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;
return {
id: "sankey",
type: "sankey",
nodes: filteredNodes.map((node) => ({
id: node.id,
value: node.value,
itemStyle: {
color: node.color,
},
depth: node.index,
})),
links,
draggable: false,
orient: this.vertical ? "vertical" : "horizontal",
nodeWidth: 15,
nodeGap: NODE_GAP,
lineStyle: {
color: "gradient",
opacity: 0.4,
},
layoutIterations: 0,
label: {
formatter: (params) =>
data.nodes.find((node) => node.id === (params.data as Node).id)
?.label ?? (params.data as Node).id,
position: this.vertical ? "bottom" : "right",
distance: LABEL_DISTANCE,
minMargin: 5,
overflow: "break",
},
labelLayout: (params) => {
if (this.vertical) {
// reduce the label font size so the longest word fits on one line
const longestWord = params.text
.split(" ")
.reduce(
(longest, current) =>
longest.length > current.length ? longest : current,
""
);
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
const fontSize = Math.min(
FONT_SIZE,
(params.rect.width / wordWidth) * FONT_SIZE
);
return {
fontSize: fontSize > 1 ? fontSize : 0,
width: params.rect.width,
align: "center",
};
}
// estimate the number of lines after the label is wrapped
// this is a very rough estimate, but it works for now
const lineCount = Math.ceil(params.labelRect.width / labelSpace);
// `overflow: "break"` allows the label to overflow outside its height, so we need to account for that
const fontSize = Math.min(
(params.rect.height / lineCount) * FONT_SIZE,
FONT_SIZE
);
return {
fontSize,
lineHeight: fontSize,
width: labelSpace,
height: params.rect.height,
};
},
top: this.vertical ? 0 : OVERFLOW_MARGIN,
bottom: this.vertical ? 25 : OVERFLOW_MARGIN,
left: this.vertical ? OVERFLOW_MARGIN : 0,
right: this.vertical ? OVERFLOW_MARGIN : labelSpace + LABEL_DISTANCE,
emphasis: {
focus: "adjacency",
},
} as SankeySeriesOption;
});
private _processLinks(nodes: Node[], rawLinks: Link[]) {
const accountedIn = new Map<string, number>();
const accountedOut = new Map<string, number>();
const links: ProcessedLink[] = [];
const passThroughNodes: Node[] = [];
rawLinks.forEach((link) => {
const sourceNode = nodes.find((n) => n.id === link.source);
const targetNode = nodes.find((n) => n.id === link.target);
@@ -222,307 +213,25 @@ export class HaSankeyChart extends LitElement {
accountedIn.set(targetNode.id, targetAccounted + value);
accountedOut.set(sourceNode.id, sourceAccounted + value);
// handle links across sections
const sourceIndex = indexes.findIndex((i) => i === sourceNode.index);
const targetIndex = indexes.findIndex((i) => i === targetNode.index);
const passThroughSections = indexes.slice(sourceIndex + 1, targetIndex);
// create pass-through nodes to reserve space
const passThroughNodeIds = passThroughSections.map((index) => {
const node = {
passThrough: true,
id: `${sourceNode.id}-${targetNode.id}-${index}`,
value,
index,
};
passThroughNodes.push(node);
return node.id;
});
if (value > 0) {
links.push({
...link,
value,
offset: {
source: sourceAccounted / (sourceNode.value || 1),
target: targetAccounted / (targetNode.value || 1),
},
passThroughNodeIds,
});
}
});
return { links, passThroughNodes };
}
private _processNodes(filteredNodes: Node[], indexes: number[]) {
// add MIN_DISTANCE as padding
const sectionSize = this.vertical
? this._sizeController.value!.width - MIN_DISTANCE * 2
: this._sizeController.value!.height - MIN_DISTANCE * 2;
const nodesPerSection: Record<number, Node[]> = {};
filteredNodes.forEach((node) => {
if (!nodesPerSection[node.index]) {
nodesPerSection[node.index] = [node];
} else {
nodesPerSection[node.index].push(node);
}
});
const sectionFlexSize = this._getSectionFlexSize(
Object.values(nodesPerSection)
);
const sections: Section[] = indexes.map((index, i) => {
const nodes: ProcessedNode[] = nodesPerSection[index].map(
(node: Node) => ({
...node,
color: node.color || DEFAULT_COLOR,
x: 0,
y: 0,
size: 0,
})
);
const availableSpace =
sectionSize - (nodes.length * MIN_DISTANCE - MIN_DISTANCE);
const totalValue = nodes.reduce(
(acc: number, node: Node) => acc + node.value,
0
);
const { nodes: sizedNodes, statePerPixel } = this._setNodeSizes(
nodes,
availableSpace,
totalValue
);
return {
nodes: sizedNodes,
offset: sectionFlexSize * i,
index,
totalValue,
statePerPixel,
};
});
sections.forEach((section) => {
// calc sizes again with the best statePerPixel
let totalSize = 0;
if (section.statePerPixel !== this._statePerPixel) {
section.nodes.forEach((node) => {
const size = Math.max(
MIN_SIZE,
Math.floor(node.value / this._statePerPixel)
);
totalSize += size;
node.size = size;
});
} else {
totalSize = section.nodes.reduce((sum, b) => sum + b.size, 0);
}
// calc margin between boxes
const emptySpace = sectionSize - totalSize;
const spacerSize = emptySpace / (section.nodes.length - 1);
// account for MIN_DISTANCE padding and center single node sections
let offset =
section.nodes.length > 1 ? MIN_DISTANCE : emptySpace / 2 + MIN_DISTANCE;
// calc positions - swap x/y for vertical layout
section.nodes.forEach((node) => {
if (this.vertical) {
node.x = offset;
node.y = section.offset;
} else {
node.x = section.offset;
node.y = offset;
}
offset += node.size + spacerSize;
});
});
return sections.flatMap((section) => section.nodes);
}
private _processPaths(nodes: ProcessedNode[], links: ProcessedLink[]) {
const flowDirection = this.vertical ? "y" : "x";
const orthDirection = this.vertical ? "x" : "y"; // orthogonal to the flow
const nodesById = new Map(nodes.map((n) => [n.id, n]));
return links.map((link) => {
const { source, target, value, offset, passThroughNodeIds } = link;
const pathNodes = [source, ...passThroughNodeIds, target].map(
(id) => nodesById.get(id)!
);
const offsets = [
offset.source,
...link.passThroughNodeIds.map(() => 0),
offset.target,
];
const sourceNode = pathNodes[0];
const targetNode = pathNodes[pathNodes.length - 1];
let path: [string, number, number][] = [
[
"M",
sourceNode[flowDirection] + NODE_WIDTH,
sourceNode[orthDirection] + offset.source * sourceNode.size,
],
]; // starting point
// traverse the path forwards. stop before the last node
for (let i = 0; i < pathNodes.length - 1; i++) {
const node = pathNodes[i];
const nextNode = pathNodes[i + 1];
const flowMiddle =
(nextNode[flowDirection] - node[flowDirection]) / 2 +
node[flowDirection];
const orthStart = node[orthDirection] + offsets[i] * node.size;
const orthEnd =
nextNode[orthDirection] + offsets[i + 1] * nextNode.size;
path.push(
["L", node[flowDirection] + NODE_WIDTH, orthStart],
["C", flowMiddle, orthStart],
["", flowMiddle, orthEnd],
["", nextNode[flowDirection], orthEnd]
);
}
// traverse the path backwards. stop before the first node
for (let i = pathNodes.length - 1; i > 0; i--) {
const node = pathNodes[i];
const prevNode = pathNodes[i - 1];
const flowMiddle =
(node[flowDirection] - prevNode[flowDirection]) / 2 +
prevNode[flowDirection];
const orthStart =
node[orthDirection] +
offsets[i] * node.size +
Math.max((value / (node.value || 1)) * node.size, 0);
const orthEnd =
prevNode[orthDirection] +
offsets[i - 1] * prevNode.size +
Math.max((value / (prevNode.value || 1)) * prevNode.size, 0);
path.push(
["L", node[flowDirection], orthStart],
["C", flowMiddle, orthStart],
["", flowMiddle, orthEnd],
["", prevNode[flowDirection] + NODE_WIDTH, orthEnd]
);
}
if (this.vertical) {
// Just swap x and y coordinates for vertical layout
path = path.map((c) => [c[0], c[2], c[1]]);
}
return {
sourceNode,
targetNode,
value,
path,
};
});
}
private _setNodeSizes(
nodes: ProcessedNode[],
availableSpace: number,
totalValue: number
): { nodes: ProcessedNode[]; statePerPixel: number } {
const statePerPixel = totalValue / availableSpace;
if (statePerPixel > this._statePerPixel) {
this._statePerPixel = statePerPixel;
}
let deficitHeight = 0;
const result = nodes.map((node) => {
if (node.size === MIN_SIZE) {
return node;
}
let size = Math.floor(node.value / this._statePerPixel);
if (size < MIN_SIZE) {
deficitHeight += MIN_SIZE - size;
size = MIN_SIZE;
}
return {
...node,
size,
};
});
if (deficitHeight > 0) {
return this._setNodeSizes(
result,
availableSpace - deficitHeight,
totalValue
);
}
return { nodes: result, statePerPixel: this._statePerPixel };
}
private _getSectionFlexSize(nodesPerSection: Node[][]): number {
const fullSize = this.vertical
? this._sizeController.value!.height
: this._sizeController.value!.width;
if (nodesPerSection.length < 2) {
return fullSize;
}
let lastSectionFlexSize: number;
if (this.vertical) {
lastSectionFlexSize = FONT_SIZE * 2 + NODE_WIDTH; // estimated based on the font size + some margin
} else {
// Estimate the width needed for the last section based on label length
const lastIndex = nodesPerSection.length - 1;
const lastSectionNodes = nodesPerSection[lastIndex];
const TEXT_PADDING = 5; // Padding between node and text
lastSectionFlexSize =
lastSectionNodes.length > 0
? Math.max(
...lastSectionNodes.map(
(node) =>
NODE_WIDTH +
TEXT_PADDING +
(node.label ? measureTextWidth(node.label, FONT_SIZE) : 0)
)
)
: 0;
}
// Calculate the flex size for other sections
const remainingSize = fullSize - lastSectionFlexSize;
const flexSize = remainingSize / (nodesPerSection.length - 1);
// if the last section is bigger than the others, we make them all the same size
// this is to prevent the last section from squishing the others
return lastSectionFlexSize < flexSize
? flexSize
: fullSize / nodesPerSection.length;
}
private _getVerticalLabelFontSize(label: string, labelWidth: number): number {
// reduce the label font size so the longest word fits on one line
const longestWord = label
.split(" ")
.reduce(
(longest, current) =>
longest.length > current.length ? longest : current,
""
);
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
return Math.min(FONT_SIZE, (labelWidth / wordWidth) * FONT_SIZE);
return links;
}
static styles = css`
:host {
display: block;
flex: 1;
background: var(--ha-card-background, var(--card-background-color, #000));
overflow: hidden;
position: relative;
background: var(--ha-card-background, var(--card-background-color));
}
svg {
overflow: visible;
position: absolute;
}
.node-label {
font-size: ${FONT_SIZE}px;
fill: var(--primary-text-color, white);
}
.node-label.vertical {
position: absolute;
text-align: center;
overflow: hidden;
ha-chart-base {
width: 100%;
height: 100%;
}
`;
}

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);
@@ -64,6 +63,9 @@ export class StateHistoryChartLine extends LitElement {
@property({ type: String }) public height?: string;
@property({ attribute: "expand-legend", type: Boolean })
public expandLegend?: boolean;
@state() private _chartData: LineSeriesOption[] = [];
@state() private _entityIds: string[] = [];
@@ -72,8 +74,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 +90,104 @@ export class StateHistoryChartLine extends LitElement {
.options=${this._chartOptions}
.height=${this.height}
style=${styleMap({ height: this.height })}
@dataset-hidden=${this._datasetHidden}
@dataset-unhidden=${this._datasetUnhidden}
.expandLegend=${this.expandLegend}
></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,58 +213,67 @@ 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,
formatter: (value: number) => {
const label = formatNumber(value, this.hass.locale);
const formatOptions =
value >= 1 || value <= -1
? undefined
: {
// show the first significant digit for tiny values
maximumFractionDigits: Math.max(
2,
-Math.floor(Math.log10(Math.abs(value % 1 || 1)))
),
};
const label = formatNumber(
value,
this.hass.locale,
formatOptions
);
const width = measureTextWidth(label, 12) + 5;
if (width > this._yWidth) {
this._yWidth = width;
@@ -218,46 +288,18 @@ export class StateHistoryChartLine extends LitElement {
} as YAXisOption,
legend: {
show: this.showNames,
icon: "circle",
padding: [20, 0],
},
grid: {
...(this.showNames ? {} : { top: 30 }), // undefined is the same as 0
top: 15,
left: rtl ? 1 : Math.max(this.paddingYAxis, this._yWidth),
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
bottom: 30,
bottom: 20,
},
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 +349,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 +424,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 +449,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 +464,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 +550,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 +580,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 +646,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 +715,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);

View File

@@ -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,
});
}
}

View File

@@ -71,6 +71,9 @@ export class StateHistoryCharts extends LitElement {
@property({ type: String }) public height?: string;
@property({ attribute: "expand-legend", type: Boolean })
public expandLegend?: boolean;
private _computedStartTime!: Date;
private _computedEndTime!: Date;
@@ -135,7 +138,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}
@@ -154,10 +157,11 @@ export class StateHistoryCharts extends LitElement {
.fitYData=${this.fitYData}
@y-width-changed=${this._yWidthChanged}
.height=${this.virtualize ? undefined : this.height}
.expandLegend=${this.expandLegend}
></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,7 +303,12 @@ export class StateHistoryCharts extends LitElement {
.entry-container {
width: 100%;
}
.entry-container.line {
flex: 1;
padding-top: 8px;
overflow: hidden;
}
.entry-container:hover {
@@ -313,6 +322,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;

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