Compare commits

...

100 Commits

Author SHA1 Message Date
Wendelin 188e82fa02 Merge branch 'dev' of github.com:home-assistant/frontend into user-siderbar 2025-02-24 09:58:13 +01:00
Joakim Sø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
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 2fba41b8ca Update translations 2025-02-19 14:06:34 +01: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
Wendelin 25a14c87a5 Delay skeleton loading to prevent flickering 2025-02-18 15:18:53 +01: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
Wendelin 18b2360e46 Fix memoize function 2025-02-18 06:29:17 +01: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
Wendelin c1a214d1af Update code format 2025-02-17 15:56:23 +01:00
Wendelin 6123d932e1 Add loading skeleton 2025-02-17 15:49:05 +01:00
Wendelin a3dcf77f2a Add profile settings for device sidebar 2025-02-17 15:16:34 +01:00
Wendelin 7d7f8a9bc2 Use user preferences for sidebar 2025-02-17 12:56:20 +01:00
Paul 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
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
117 changed files with 4836 additions and 1859 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"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",
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# This script will run after the container is created
# add github cli
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
# Update package lists
sudo apt-get update
sudo apt upgrade -y
# Install necessary packages
sudo apt-get install -y libpcap-dev gh
# Display a message
echo "Post-create script has been executed successfully."
+3 -3
View File
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.2.0
uses: actions/cache@v4.2.1
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
+2 -2
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
+42
View File
@@ -1,6 +1,42 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Develop and serve Frontend",
"type": "shell",
"command": "script/develop_and_serve -c ${input:coreUrl}",
// Sync changes here to other tasks until issue resolved
// https://github.com/Microsoft/vscode/issues/61497
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Develop Frontend",
"type": "gulp",
@@ -241,6 +277,12 @@
"id": "supervisorToken",
"type": "promptString",
"description": "The token for the Remote API proxy add-on"
},
{
"id": "coreUrl",
"type": "promptString",
"description": "The URL of the Home Assistant Core instance",
"default": "http://127.0.0.1:8123"
}
]
}
+13 -13
View File
@@ -1,16 +1,16 @@
// @ts-check
import tseslint from "typescript-eslint";
import rootConfig from "../eslint.config.mjs";
export default [
...rootConfig,
{
rules: {
"no-console": "off",
"import/no-extraneous-dependencies": "off",
"import/extensions": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
"@typescript-eslint/no-require-imports": "off",
"prefer-arrow-callback": "off",
},
export default tseslint.config(...rootConfig, {
rules: {
"no-console": "off",
"import/no-extraneous-dependencies": "off",
"import/extensions": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
"@typescript-eslint/no-require-imports": "off",
"prefer-arrow-callback": "off",
},
];
});
+8
View File
@@ -90,6 +90,14 @@ function copyMapPanel(staticDir) {
npmPath("leaflet/dist/leaflet.css"),
staticPath("images/leaflet/")
);
copyFileDir(
npmPath("leaflet.markercluster/dist/MarkerCluster.css"),
staticPath("images/leaflet/")
);
copyFileDir(
npmPath("leaflet.markercluster/dist/MarkerCluster.Default.css"),
staticPath("images/leaflet/")
);
fs.copySync(
npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/")
+17 -15
View File
@@ -1,11 +1,16 @@
// @ts-check
/* eslint-disable import/no-extraneous-dependencies */
import unusedImports from "eslint-plugin-unused-imports";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";
import { configs as litConfigs } from "eslint-plugin-lit";
import { configs as wcConfigs } from "eslint-plugin-wc";
const _filename = fileURLToPath(import.meta.url);
const _dirname = path.dirname(_filename);
@@ -15,17 +20,14 @@ const compat = new FlatCompat({
allConfig: js.configs.all,
});
export default [
...compat.extends(
"airbnb-base",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/strict",
"plugin:@typescript-eslint/stylistic",
"plugin:wc/recommended",
"plugin:lit/all",
"plugin:lit-a11y/recommended",
"prettier"
),
export default tseslint.config(
...compat.extends("airbnb-base", "plugin:lit-a11y/recommended"),
eslintConfigPrettier,
litConfigs["flat/all"],
tseslint.configs.recommended,
tseslint.configs.strict,
tseslint.configs.stylistic,
wcConfigs["flat/recommended"],
{
plugins: {
"unused-imports": unusedImports,
@@ -43,7 +45,7 @@ export default [
Polymer: true,
},
parser: tsParser,
parser: tseslint.parser,
ecmaVersion: 2020,
sourceType: "module",
@@ -184,5 +186,5 @@ export default [
],
"no-use-before-define": "off",
},
},
];
}
);
+7 -7
View File
@@ -1,10 +1,10 @@
// @ts-check
import tseslint from "typescript-eslint";
import rootConfig from "../eslint.config.mjs";
export default [
...rootConfig,
{
rules: {
"no-console": "off",
},
export default tseslint.config(...rootConfig, {
rules: {
"no-console": "off",
},
];
});
@@ -17,6 +17,7 @@ import "../../../src/components/ha-alert";
import {
ALTERNATIVE_DNS_SERVERS,
getSupervisorNetworkInfo,
pingSupervisor,
setSupervisorNetworkDns,
} from "../data/supervisor";
import { fireEvent } from "../../../src/common/dom/fire_event";
@@ -85,7 +86,28 @@ class LandingPageNetwork extends LitElement {
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
this._fetchSupervisorInfo();
this._pingSupervisor();
}
private _schedulePingSupervisor() {
setTimeout(
() => this._pingSupervisor(),
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
);
}
private async _pingSupervisor() {
try {
const response = await pingSupervisor();
if (!response.ok) {
throw new Error("Failed to ping supervisor, assume update in progress");
}
this._fetchSupervisorInfo();
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
this._schedulePingSupervisor();
}
}
private _scheduleFetchSupervisorInfo() {
+8 -4
View File
@@ -18,7 +18,7 @@ export const ALTERNATIVE_DNS_SERVERS: {
];
export async function getSupervisorLogs(lines = 100) {
return fetch(`/supervisor/supervisor/logs?lines=${lines}`, {
return fetch(`/supervisor-api/supervisor/logs?lines=${lines}`, {
headers: {
Accept: "text/plain",
},
@@ -26,22 +26,26 @@ export async function getSupervisorLogs(lines = 100) {
}
export async function getSupervisorLogsFollow(lines = 500) {
return fetch(`/supervisor/supervisor/logs/follow?lines=${lines}`, {
return fetch(`/supervisor-api/supervisor/logs/follow?lines=${lines}`, {
headers: {
Accept: "text/plain",
},
});
}
export async function pingSupervisor() {
return fetch("/supervisor-api/supervisor/ping");
}
export async function getSupervisorNetworkInfo() {
return fetch("/supervisor/network/info");
return fetch("/supervisor-api/network/info");
}
export const setSupervisorNetworkDns = async (
dnsServerIndex: number,
primaryInterface: string
) =>
fetch(`/supervisor/network/interface/${primaryInterface}/update`, {
fetch(`/supervisor-api/network/interface/${primaryInterface}/update`, {
method: "POST",
body: JSON.stringify({
ipv4: {
+42 -39
View File
@@ -26,25 +26,25 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.26.7",
"@babel/runtime": "7.26.9",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.4",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/commands": "6.8.0",
"@codemirror/language": "6.10.8",
"@codemirror/legacy-modes": "6.4.2",
"@codemirror/search": "6.5.8",
"@codemirror/legacy-modes": "6.4.3",
"@codemirror/search": "6.5.9",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.2",
"@codemirror/view": "6.36.3",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.17.2",
"@formatjs/intl-displaynames": "6.8.9",
"@formatjs/intl-durationformat": "0.7.2",
"@formatjs/intl-datetimeformat": "6.17.3",
"@formatjs/intl-displaynames": "6.8.10",
"@formatjs/intl-durationformat": "0.7.3",
"@formatjs/intl-getcanonicallocales": "2.5.4",
"@formatjs/intl-listformat": "7.7.9",
"@formatjs/intl-locale": "4.2.9",
"@formatjs/intl-numberformat": "8.15.2",
"@formatjs/intl-pluralrules": "5.4.2",
"@formatjs/intl-relativetimeformat": "11.4.9",
"@formatjs/intl-listformat": "7.7.10",
"@formatjs/intl-locale": "4.2.10",
"@formatjs/intl-numberformat": "8.15.3",
"@formatjs/intl-pluralrules": "5.4.3",
"@formatjs/intl-relativetimeformat": "11.4.10",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -53,9 +53,9 @@
"@fullcalendar/timegrid": "6.1.15",
"@lezer/highlight": "1.2.1",
"@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.7",
"@lit-labs/observers": "2.0.4",
"@lit-labs/virtualizer": "2.0.15",
"@lit-labs/motion": "1.0.8",
"@lit-labs/observers": "2.0.5",
"@lit-labs/virtualizer": "2.1.0",
"@lrnwebcomponents/simple-tooltip": "8.0.2",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
@@ -90,9 +90,10 @@
"@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.4",
"@vaadin/vaadin-themable-mixin": "24.6.4",
"@vaadin/combo-box": "24.6.5",
"@vaadin/vaadin-themable-mixin": "24.6.5",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.9",
@@ -116,16 +117,18 @@
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.7.14",
"intl-messageformat": "10.7.15",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"leaflet.markercluster": "1.5.3",
"lit": "2.8.0",
"lit-html": "2.8.0",
"luxon": "3.5.0",
"marked": "15.0.6",
"marked": "15.0.7",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
"punycode": "2.3.1",
"qr-scanner": "1.4.2",
"qrcode": "1.5.4",
@@ -137,7 +140,7 @@
"tinykeys": "3.0.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "2.0.1",
"ua-parser-js": "2.0.2",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
"vue": "2.7.16",
@@ -152,20 +155,20 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.26.7",
"@babel/core": "7.26.9",
"@babel/helper-define-polyfill-provider": "0.6.3",
"@babel/plugin-proposal-decorators": "7.25.9",
"@babel/plugin-transform-runtime": "7.25.9",
"@babel/preset-env": "7.26.7",
"@babel/plugin-transform-runtime": "7.26.9",
"@babel/preset-env": "7.26.9",
"@babel/preset-typescript": "7.26.0",
"@bundle-stats/plugin-webpack-filter": "4.18.2",
"@lokalise/node-api": "13.0.0",
"@octokit/auth-oauth-device": "7.1.2",
"@octokit/plugin-retry": "7.1.3",
"@octokit/rest": "21.1.0",
"@lokalise/node-api": "13.2.0",
"@octokit/auth-oauth-device": "7.1.3",
"@octokit/plugin-retry": "7.1.4",
"@octokit/rest": "21.1.1",
"@rsdoctor/rspack-plugin": "0.4.13",
"@rspack/cli": "1.2.2",
"@rspack/core": "1.2.2",
"@rspack/cli": "1.2.5",
"@rspack/core": "1.2.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-sender": "1.0.11",
@@ -175,6 +178,7 @@
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.16",
"@types/leaflet-draw": "1.0.11",
"@types/leaflet.markercluster": "1.5.5",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2",
"@types/mocha": "10.0.10",
@@ -183,14 +187,12 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "8.23.0",
"@typescript-eslint/parser": "8.23.0",
"@vitest/coverage-v8": "3.0.5",
"@vitest/coverage-v8": "3.0.6",
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.20.0",
"eslint": "9.20.1",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.0.1",
"eslint-import-resolver-webpack": "0.13.10",
@@ -198,7 +200,7 @@
"eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "2.2.0",
"eslint-plugin-wc": "2.2.1",
"fancy-log": "2.0.0",
"fs-extra": "11.3.0",
"glob": "11.0.1",
@@ -215,9 +217,8 @@
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"map-stream": "0.0.7",
"object-hash": "3.0.0",
"pinst": "3.0.0",
"prettier": "3.4.2",
"prettier": "3.5.1",
"rspack-manifest-plugin": "5.0.3",
"serve": "14.2.4",
"sinon": "19.0.2",
@@ -225,7 +226,9 @@
"terser-webpack-plugin": "5.3.11",
"ts-lit-plugin": "2.0.2",
"typescript": "5.7.3",
"vitest": "3.0.5",
"typescript-eslint": "8.24.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.0.6",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -239,7 +242,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15",
"globals": "15.14.0",
"globals": "16.0.0",
"tslib": "2.8.1"
},
"packageManager": "yarn@4.6.0"
+13 -6
View File
@@ -136,11 +136,18 @@ export function theme2hex(themeColor: string): string {
}
const rgbFromColorName = colors[themeColor];
if (!rgbFromColorName) {
// We have a named color, and there's nothing in the table,
// so nothing further we can do with it.
// Compare/border/background color will all be the same.
return themeColor;
if (rgbFromColorName) {
return rgb2hex(rgbFromColorName);
}
return rgb2hex(rgbFromColorName);
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (rgbMatch) {
const [, r, g, b] = rgbMatch.map(Number);
return rgb2hex([r, g, b]);
}
// We have a named color, and there's nothing in the table,
// so nothing further we can do with it.
// Compare/border/background color will all be the same.
return themeColor;
}
+19
View File
@@ -16,11 +16,30 @@ export const setupLeafletMap = async (
const Leaflet = (await import("leaflet")).default as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
await import("leaflet.markercluster");
const map = Leaflet.map(mapElement);
const style = document.createElement("link");
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
style.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(style);
const markerClusterStyle = document.createElement("link");
markerClusterStyle.setAttribute(
"href",
"/static/images/leaflet/MarkerCluster.css"
);
markerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(markerClusterStyle);
const defaultMarkerClusterStyle = document.createElement("link");
defaultMarkerClusterStyle.setAttribute(
"href",
"/static/images/leaflet/MarkerCluster.Default.css"
);
defaultMarkerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(defaultMarkerClusterStyle);
map.setView([52.3731339, 4.8903147], 13);
const tileLayer = createTileLayer(Leaflet).addTo(map);
+1 -1
View File
@@ -1,2 +1,2 @@
export const computeDomain = (entityId: string): string =>
entityId.substr(0, entityId.indexOf("."));
entityId.substring(0, entityId.indexOf("."));
+1 -5
View File
@@ -120,11 +120,6 @@ export const computeStateDisplayFromEntityAttributes = (
return value;
}
if (domain === "datetime") {
const time = new Date(state);
return formatDateTime(time, locale, config);
}
if (["date", "input_datetime", "time"].includes(domain)) {
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
// Attributes aren't available, we have to use `state`.
@@ -181,6 +176,7 @@ export const computeStateDisplayFromEntityAttributes = (
"tag",
"tts",
"wake_word",
"datetime",
].includes(domain) ||
(domain === "sensor" && attributes.device_class === "timestamp")
) {
+17 -1
View File
@@ -1,6 +1,9 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
import { UNAVAILABLE_STATES } from "../../data/entity";
import type { HomeAssistant } from "../../types";
import { computeDomain } from "./compute_domain";
import { stringCompare } from "../string/compare";
export const FIXED_DOMAIN_STATES = {
alarm_control_panel: [
@@ -237,6 +240,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
};
export const getStates = (
hass: HomeAssistant,
state: HassEntity,
attribute: string | undefined = undefined
): string[] => {
@@ -269,7 +273,19 @@ export const getStates = (
case "device_tracker":
case "person":
if (!attribute) {
result.push("home", "not_home");
result.push(
...Object.entries(hass.states)
.filter(
([entityId, stateObj]) =>
computeDomain(entityId) === "zone" &&
entityId !== "zone.home" &&
stateObj.attributes.friendly_name
)
.map(([_entityId, stateObj]) => stateObj.attributes.friendly_name!)
.sort((zone1, zone2) =>
stringCompare(zone1, zone2, hass.locale.language)
)
);
}
break;
case "event":
+32
View File
@@ -0,0 +1,32 @@
import type { LatLngExpression, Layer, Map, MarkerOptions } from "leaflet";
import { Marker } from "leaflet";
export class DecoratedMarker extends Marker {
decorationLayer: Layer | undefined;
constructor(
latlng: LatLngExpression,
decorationLayer?: Layer,
options?: MarkerOptions
) {
super(latlng, options);
this.decorationLayer = decorationLayer;
}
onAdd(map: Map) {
super.onAdd(map);
// If decoration has been provided, add it to the map as well
this.decorationLayer?.addTo(map);
return this;
}
onRemove(map: Map) {
// If decoration has been provided, remove it from the map as well
this.decorationLayer?.remove();
return super.onRemove(map);
}
}
+49 -42
View File
@@ -24,6 +24,7 @@ import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
import "../ha-icon-button";
import { formatTimeLabel } from "./axis-label";
import { ensureArray } from "../../common/array/ensure-array";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
@@ -67,12 +68,16 @@ export class HaChartBase extends LitElement {
private _listeners: (() => void)[] = [];
private _originalZrFlush?: () => void;
public disconnectedCallback() {
super.disconnectedCallback();
while (this._listeners.length) {
this._listeners.pop()!();
}
this.chart?.dispose();
this.chart = undefined;
this._originalZrFlush = undefined;
}
public connectedCallback() {
@@ -83,19 +88,19 @@ export class HaChartBase extends LitElement {
this._listeners.push(
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
this._reducedMotion = matches;
this.chart?.setOption({ animation: !this._reducedMotion });
if (this._reducedMotion !== matches) {
this._reducedMotion = matches;
this._setChartOptions({ animation: !this._reducedMotion });
}
})
);
// Add keyboard event listeners
const handleKeyDown = (ev: KeyboardEvent) => {
if ((isMac && ev.metaKey) || (!isMac && ev.ctrlKey)) {
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
this._modifierPressed = true;
if (!this.options?.dataZoom) {
this.chart?.setOption({
dataZoom: this._getDataZoomConfig(),
});
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
}
}
};
@@ -104,9 +109,7 @@ export class HaChartBase extends LitElement {
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
this._modifierPressed = false;
if (!this.options?.dataZoom) {
this.chart?.setOption({
dataZoom: this._getDataZoomConfig(),
});
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
}
}
};
@@ -124,27 +127,24 @@ export class HaChartBase extends LitElement {
}
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this.hasUpdated || !this.chart) {
if (!this.chart) {
return;
}
if (changedProps.has("_themes")) {
this._setupChart();
return;
}
let chartOptions: ECOption = {};
if (changedProps.has("data")) {
this.chart.setOption(
{ series: this.data },
{ lazyUpdate: true, replaceMerge: ["series"] }
);
chartOptions.series = this.data;
}
if (changedProps.has("options") || changedProps.has("_isZoomed")) {
this.chart.setOption(this._createOptions(), {
lazyUpdate: true,
// if we replace the whole object, it will reset the dataZoom
replaceMerge: ["grid"],
});
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);
}
}
@@ -158,7 +158,6 @@ export class HaChartBase extends LitElement {
style=${styleMap({
height: this.height ?? `${this._getDefaultHeight()}px`,
})}
@wheel=${this._handleWheel}
>
<div class="chart"></div>
${this._isZoomed
@@ -240,8 +239,8 @@ 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,
};
}
@@ -512,25 +511,33 @@ export class HaChartBase extends LitElement {
return Math.max(this.clientWidth / 2, 200);
}
private _handleZoomReset() {
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
this._modifierPressed = false;
}
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 _setChartOptions(options: ECOption) {
if (!this.chart) {
return;
}
if (!this._originalZrFlush) {
const dataSize = ensureArray(this.data).reduce(
(acc, series) => acc + (series.data as any[]).length,
0
);
if (dataSize > 10000) {
// delay the last bit of the render to avoid blocking the main thread
// this is not that impactful with sampling enabled but it doesn't hurt to have it
const zr = this.chart.getZr();
this._originalZrFlush = zr.flush;
zr.flush = () => {
setTimeout(() => {
this._originalZrFlush?.call(zr);
}, 5);
};
}
}
const replaceMerge = options.series ? ["series"] : [];
this.chart.setOption(options, { replaceMerge });
}
private _handleZoomReset() {
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
}
static styles = css`
@@ -75,6 +75,8 @@ export class StateHistoryChartLine extends LitElement {
@state() private _yWidth = 25;
@state() private _visualMap?: VisualMapComponentOption[];
private _chartTime: Date = new Date();
protected render() {
@@ -92,7 +94,7 @@ export class StateHistoryChartLine extends LitElement {
`;
}
private _renderTooltip(params: any) {
private _renderTooltip = (params: any) => {
const time = params[0].axisValue;
const title =
formatDateTimeWithSeconds(
@@ -115,7 +117,7 @@ export class StateHistoryChartLine extends LitElement {
return;
}
// If the datapoint is not found, we need to find the last datapoint before the current time
let lastData;
let lastData: any;
const data = dataset.data || [];
for (let i = data.length - 1; i >= 0; i--) {
const point = data[i];
@@ -175,7 +177,7 @@ export class StateHistoryChartLine extends LitElement {
})
.join("<br>")
);
}
};
private _datasetHidden(ev: CustomEvent) {
this._hiddenStats.add(ev.detail.name);
@@ -208,8 +210,8 @@ 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 rtl = computeRTL(this.hass);
@@ -280,37 +282,11 @@ export class StateHistoryChartLine extends LitElement {
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
bottom: 30,
},
visualMap: this._chartData
.map((_, seriesIndex) => {
const dataIndex = this._datasetToDataIndex[seriesIndex];
const data = this.data[dataIndex];
if (!data.statistics || data.statistics.length === 0) {
return false;
}
// render stat data with a slightly transparent line
const firstStateTS =
data.states[0]?.last_changed ?? this.endTime.getTime();
return {
show: false,
seriesIndex,
dimension: 0,
pieces: [
{
max: firstStateTS - 0.01,
colorAlpha: 0.5,
},
{
min: firstStateTS,
colorAlpha: 1,
},
],
};
})
.filter(Boolean) as VisualMapComponentOption[],
visualMap: this._visualMap,
tooltip: {
trigger: "axis",
appendTo: document.body,
formatter: this._renderTooltip.bind(this),
formatter: this._renderTooltip,
},
};
}
@@ -378,9 +354,10 @@ export class StateHistoryChartLine extends LitElement {
name: nameY,
color,
symbol: "circle",
step: "end",
animationDurationUpdate: 0,
symbolSize: 1,
step: "end",
sampling: "minmax",
animationDurationUpdate: 0,
lineStyle: {
width: fill ? 0 : 1.5,
},
@@ -725,6 +702,33 @@ 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)) {
+5 -5
View File
@@ -273,11 +273,13 @@ export class StatisticsChart extends LitElement {
this._chartOptions = {
xAxis: [
{
id: "xAxis",
type: "time",
min: startTime,
max: endTime,
max: this.endTime,
},
{
id: "hiddenAxis",
type: "time",
show: false,
},
@@ -368,7 +370,6 @@ export class StatisticsChart extends LitElement {
if (endTime > new Date()) {
endTime = new Date();
}
this.endTime = endTime;
let unit: string | undefined | null;
@@ -491,8 +492,8 @@ export class StatisticsChart extends LitElement {
: this.hass.localize(
`ui.components.statistics_charts.statistic_types.${type}`
),
symbol: "circle",
symbolSize: 0,
symbol: "none",
sampling: "minmax",
animationDurationUpdate: 0,
lineStyle: {
width: 1.5,
@@ -510,7 +511,6 @@ export class StatisticsChart extends LitElement {
if (band && this.chartType === "line") {
series.stack = `band-${statistic_id}`;
series.stackStrategy = "all";
(series as LineSeriesOption).symbol = "none";
if (drawBands && type === "max") {
(series as LineSeriesOption).areaStyle = {
color: color + "3F",
@@ -448,6 +448,7 @@ export class HaDataTable extends LitElement {
)}
@click=${this._handleHeaderClick}
.columnId=${key}
title=${ifDefined(column.title)}
>
${column.sortable
? html`
@@ -57,7 +57,7 @@ class HaEntityStatePicker extends LitElement {
(this._comboBox as any).items = [
...(this.extraOptions ?? []),
...(this.entityId && stateObj
? getStates(stateObj, this.attribute).map((key) => ({
? getStates(this.hass, stateObj, this.attribute).map((key) => ({
value: key,
label: !this.attribute
? this.hass.formatEntityState(stateObj, key)
+110
View File
@@ -0,0 +1,110 @@
import { customElement, property, state } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
import "./ha-button";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-textfield";
import type { HomeAssistant } from "../types";
import { copyToClipboard } from "../common/util/copy-clipboard";
import { showToast } from "../util/toast";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-copy-textfield")
export class HaCopyTextfield extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "value" }) public value!: string;
@property({ attribute: "masked-value" }) public maskedValue?: string;
@property({ attribute: "label" }) public label?: string;
@state() private _showMasked = true;
public render() {
return html`
<div class="container">
<div class="textfield-container">
<ha-textfield
.value=${this._showMasked && this.maskedValue
? this.maskedValue
: this.value}
readonly
.suffix=${this.maskedValue
? html`<div style="width: 24px"></div>`
: nothing}
@click=${this._focusInput}
></ha-textfield>
${this.maskedValue
? html`<ha-icon-button
class="toggle-unmasked"
.label=${this.hass.localize(
`ui.common.${this._showMasked ? "show" : "hide"}`
)}
@click=${this._toggleMasked}
.path=${this._showMasked ? mdiEye : mdiEyeOff}
></ha-icon-button>`
: nothing}
</div>
<ha-button @click=${this._copy} unelevated>
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
${this.label || this.hass.localize("ui.common.copy")}
</ha-button>
</div>
`;
}
private _focusInput(ev) {
const inputElement = ev.currentTarget as HaTextField;
inputElement.select();
}
private _toggleMasked(): void {
this._showMasked = !this._showMasked;
}
private async _copy(): Promise<void> {
await copyToClipboard(this.value);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static styles = css`
.container {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.textfield-container {
position: relative;
flex: 1;
}
.textfield-container ha-textfield {
display: block;
}
.toggle-unmasked {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-copy-textfield": HaCopyTextfield;
}
}
+135 -33
View File
@@ -5,15 +5,16 @@ import "@material/mwc-list/mwc-list-item";
import { mdiCalendar } from "@mdi/js";
import {
addDays,
subHours,
endOfDay,
endOfMonth,
endOfWeek,
endOfYear,
isThisYear,
startOfDay,
startOfMonth,
startOfWeek,
startOfYear,
isThisYear,
} from "date-fns";
import { fromZonedTime, toZonedTime } from "date-fns-tz";
import type { PropertyValues, TemplateResult } from "lit";
@@ -178,6 +179,96 @@ export class HaDateRangePicker extends LitElement {
weekStartsOn,
}),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-1h"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
1
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-12h"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
12
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-24h"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
24
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-7d"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
24 * 7
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-30d"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
24 * 30
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
}
: {}),
};
@@ -395,44 +486,55 @@ export class HaDateRangePicker extends LitElement {
}
static styles = css`
ha-icon-button {
direction: var(--direction);
}
ha-icon-button {
direction: var(--direction);
.date-range-inputs {
display: flex;
align-items: center;
gap: 8px;
}
.date-range-ranges {
border-right: 1px solid var(--divider-color);
}
.date-range-footer {
display: flex;
justify-content: flex-end;
padding: 8px;
border-top: 1px solid var(--divider-color);
}
ha-textarea {
display: inline-block;
width: 340px;
}
@media only screen and (max-width: 460px) {
ha-textarea {
width: 100%;
}
.date-range-inputs {
display: flex;
align-items: center;
gap: 8px;
}
}
@media only screen and (max-width: 800px) {
.date-range-ranges {
border-right: 1px solid var(--divider-color);
border-right: none;
border-bottom: 1px solid var(--divider-color);
}
}
@media only screen and (max-height: 940px) and (max-width: 800px) {
.date-range-ranges {
overflow: auto;
max-height: calc(70vh - 330px);
min-height: 160px;
}
.date-range-footer {
display: flex;
justify-content: flex-end;
padding: 8px;
border-top: 1px solid var(--divider-color);
:host([header-position]) .date-range-ranges {
max-height: calc(90vh - 430px);
}
ha-textarea {
display: inline-block;
width: 340px;
}
@media only screen and (max-width: 460px) {
ha-textarea {
width: 100%
}
@media only screen and (max-width: 800px) {
.date-range-ranges {
border-right: none;
border-bottom: 1px solid var(--divider-color);
}
}
`;
}
`;
}
declare global {
-1
View File
@@ -80,7 +80,6 @@ export class HaFormString extends LitElement implements HaFormElement {
if (!this.isPassword) return nothing;
return html`
<ha-icon-button
toggles
.label=${this.localize?.(
`${this.localizeBaseKey}.${
this.unmaskedPassword ? "hide_password" : "show_password"
+33
View File
@@ -1,7 +1,12 @@
import type { PropertyValues } from "lit";
import { ReactiveElement } from "lit";
import { customElement, property } from "lit/decorators";
import hash from "object-hash";
import { fireEvent } from "../common/dom/fire_event";
import { renderMarkdown } from "../resources/render-markdown";
import { CacheManager } from "../util/cache-manager";
const markdownCache = new CacheManager<string>(1000);
const _gitHubMarkdownAlerts = {
reType:
@@ -26,6 +31,16 @@ class HaMarkdownElement extends ReactiveElement {
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
false;
@property({ type: Boolean }) public cache = false;
public disconnectedCallback() {
super.disconnectedCallback();
if (this.cache) {
const key = this._computeCacheKey();
markdownCache.set(key, this.innerHTML);
}
}
protected createRenderRoot() {
return this;
}
@@ -37,6 +52,24 @@ class HaMarkdownElement extends ReactiveElement {
}
}
protected willUpdate(_changedProperties: PropertyValues): void {
if (!this.innerHTML && this.cache) {
const key = this._computeCacheKey();
if (markdownCache.has(key)) {
this.innerHTML = markdownCache.get(key)!;
this._resize();
}
}
}
private _computeCacheKey() {
return hash({
content: this.content,
allowSvg: this.allowSvg,
breaks: this.breaks,
});
}
private async _render() {
this.innerHTML = await renderMarkdown(
String(this.content),
+3
View File
@@ -13,6 +13,8 @@ export class HaMarkdown extends LitElement {
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
false;
@property({ type: Boolean }) public cache = false;
protected render() {
if (!this.content) {
return nothing;
@@ -23,6 +25,7 @@ export class HaMarkdown extends LitElement {
.allowSvg=${this.allowSvg}
.breaks=${this.breaks}
.lazyImages=${this.lazyImages}
.cache=${this.cache}
></ha-markdown-element>`;
}
-1
View File
@@ -132,7 +132,6 @@ export class HaPasswordField extends LitElement {
@change=${this._handleChangeEvent}
></ha-textfield>
<ha-icon-button
toggles
.label=${this.hass?.localize(
this._unmaskedPassword
? "ui.components.selectors.text.hide_password"
@@ -95,7 +95,6 @@ export class HaTextSelector extends LitElement {
></ha-textfield>
${this.selector.text?.type === "password"
? html`<ha-icon-button
toggles
.label=${this.hass?.localize(
this._unmaskedPassword
? "ui.components.selectors.text.hide_password"
+198 -51
View File
@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@shoelace-style/shoelace/dist/components/skeleton/skeleton";
import {
mdiBell,
mdiCalendar,
@@ -49,6 +50,10 @@ import "./ha-sortable";
import "./ha-svg-icon";
import "./user/ha-user-badge";
import { preventDefault } from "../common/dom/prevent_default";
import {
saveSidebarPreferences,
subscribeSidebarPreferences,
} from "../data/sidebar";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
@@ -207,30 +212,40 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
@storage({
key: "sidebarPanelOrder",
state: true,
subscribe: true,
})
private _panelOrder: string[] = [];
@storage({ key: "sidebarPanelOrder", state: true, subscribe: true })
private _devicePanelOrder?: string[];
@storage({
key: "sidebarHiddenPanels",
state: true,
subscribe: true,
})
private _hiddenPanels: string[] = [];
@storage({ key: "sidebarHiddenPanels", state: true, subscribe: true })
private _deviceHiddenPanels?: string[];
@state()
private _userPanelOrder: string[] = [];
@state()
private _userHiddenPanels: string[] = [];
@state()
private _loadingUserPreferences = true;
public hassSubscribe(): UnsubscribeFunc[] {
return this.hass.user?.is_admin
? [
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._issuesCount = repairs.issues.filter(
(issue) => !issue.ignored
).length;
}),
]
: [];
const subscribeFunctions = [
subscribeSidebarPreferences(this.hass, (sidebar) => {
this._userPanelOrder = sidebar?.panelOrder || [];
this._userHiddenPanels = sidebar?.hiddenPanels || [];
this._loadingUserPreferences = false;
}),
];
if (this.hass.user?.is_admin) {
subscribeFunctions.push(
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._issuesCount = repairs.issues.filter(
(issue) => !issue.ignored
).length;
})
);
}
return subscribeFunctions;
}
protected render() {
@@ -260,8 +275,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
changedProps.has("_updatesCount") ||
changedProps.has("_issuesCount") ||
changedProps.has("_notifications") ||
changedProps.has("_hiddenPanels") ||
changedProps.has("_panelOrder")
changedProps.has("_devicePanelOrder") ||
changedProps.has("_deviceHiddenPanels") ||
changedProps.has("_userPanelOrder") ||
changedProps.has("_userHiddenPanels")
) {
return true;
}
@@ -381,12 +398,51 @@ class HaSidebar extends SubscribeMixin(LitElement) {
</div>`;
}
private _getPanelPreferencesMemoized = memoizeOne(
(
userPanelOrder: string[],
userHiddenPanels: string[],
userPreferencesLoading: boolean,
devicePanelOrder?: string[],
deviceHiddenPanels?: string[]
): { panelOrder: string[]; hiddenPanels: string[]; loading: boolean } => {
let panelOrder = userPanelOrder ?? [];
let hiddenPanels = userHiddenPanels ?? [];
let loading = userPreferencesLoading;
if (devicePanelOrder || deviceHiddenPanels) {
panelOrder = devicePanelOrder ?? [];
hiddenPanels = deviceHiddenPanels ?? [];
loading = false;
}
return {
panelOrder,
hiddenPanels,
loading,
};
}
);
private _getPanelPreferences() {
return this._getPanelPreferencesMemoized(
this._userPanelOrder,
this._userHiddenPanels,
this._loadingUserPreferences,
this._devicePanelOrder,
this._deviceHiddenPanels
);
}
private _renderAllPanels() {
const { panelOrder, hiddenPanels, loading } = this._getPanelPreferences();
const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels,
panelOrder,
hiddenPanels,
this.hass.locale
);
@@ -407,12 +463,21 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@keydown=${this._listboxKeydown}
@iron-activate=${preventDefault}
>
${this.editMode
? this._renderPanelsEdit(beforeSpacer)
: this._renderPanels(beforeSpacer)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
${loading ? html`
<div class="loading">
<sl-skeleton effect="sheen"></sl-skeleton>
<sl-skeleton effect="sheen"></sl-skeleton>
<sl-skeleton effect="sheen"></sl-skeleton>
<sl-skeleton effect="sheen"></sl-skeleton>
</div>
` : html`
${this.editMode
? this._renderPanelsEdit(beforeSpacer)
: this._renderPanels(beforeSpacer)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
`}
</paper-listbox>
`;
}
@@ -474,23 +539,49 @@ class HaSidebar extends SubscribeMixin(LitElement) {
`;
}
private async _setPanelOrder(panelOrder: string[]) {
if (this._devicePanelOrder || this._deviceHiddenPanels) {
this._devicePanelOrder = [...panelOrder];
} else {
this._userPanelOrder = [...panelOrder];
await saveSidebarPreferences(this.hass, {
panelOrder: panelOrder,
hiddenPanels: this._userHiddenPanels,
});
}
}
private async _setHiddenPanels(hiddenPanels: string[]) {
if (this._devicePanelOrder || this._deviceHiddenPanels) {
this._deviceHiddenPanels = hiddenPanels;
} else {
this._userHiddenPanels = hiddenPanels;
await saveSidebarPreferences(this.hass, {
panelOrder: this._userPanelOrder,
hiddenPanels: hiddenPanels,
});
}
}
private _panelMoved(ev: CustomEvent) {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const { panelOrder, hiddenPanels } = this._getPanelPreferences();
const [beforeSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels,
panelOrder,
hiddenPanels!,
this.hass.locale
);
const panelOrder = beforeSpacer.map((panel) => panel.url_path);
const panel = panelOrder.splice(oldIndex, 1)[0];
panelOrder.splice(newIndex, 0, panel);
const panelOrderNew = beforeSpacer.map((panel) => panel.url_path);
const panel = panelOrderNew.splice(oldIndex, 1)[0];
panelOrderNew.splice(newIndex, 0, panel);
this._panelOrder = panelOrder;
this._setPanelOrder(panelOrderNew);
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
@@ -507,8 +598,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
private _renderHiddenPanels() {
return html`${this._hiddenPanels.length
? html`${this._hiddenPanels.map((url) => {
const { hiddenPanels } = this._getPanelPreferences();
return html`${hiddenPanels.length
? html`${hiddenPanels.map((url) => {
const panel = this.hass.panels[url];
if (!panel) {
return "";
@@ -690,9 +783,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
}
private get _tooltip() {
@@ -730,21 +821,25 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private async _hidePanel(ev: Event) {
ev.preventDefault();
const panel = (ev.currentTarget as any).panel;
if (this._hiddenPanels.includes(panel)) {
if ((this._deviceHiddenPanels || this._userHiddenPanels).includes(panel)) {
return;
}
const { panelOrder, hiddenPanels } = this._getPanelPreferences();
// Make a copy for Memoize
this._hiddenPanels = [...this._hiddenPanels, panel];
this._setHiddenPanels([...hiddenPanels, panel]);
// Remove it from the panel order
this._panelOrder = this._panelOrder.filter((order) => order !== panel);
this._setPanelOrder(panelOrder.filter((order) => order !== panel));
}
private async _unhidePanel(ev: Event) {
ev.preventDefault();
const panel = (ev.currentTarget as any).panel;
this._hiddenPanels = this._hiddenPanels.filter(
(hidden) => hidden !== panel
);
const { hiddenPanels } = this._getPanelPreferences();
this._setHiddenPanels(hiddenPanels.filter((hidden) => hidden !== panel));
}
private _itemMouseEnter(ev: MouseEvent) {
@@ -784,9 +879,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._hideTooltip();
}
@eventOptions({
passive: true,
})
@eventOptions({ passive: true })
private _listboxScroll() {
// On keypresses on the listbox, we're going to ignore scroll events
// for 100ms so that if pressing down arrow scrolls the sidebar, the tooltip
@@ -1117,6 +1210,60 @@ class HaSidebar extends SubscribeMixin(LitElement) {
-webkit-transform: scaleX(var(--scale-direction));
transform: scaleX(var(--scale-direction));
}
@keyframes skeletonAnimate {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes contentAnimate {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.loading {
opacity: 0;
animation-name: skeletonAnimate;
animation-duration: 2000ms;
animation-delay: 0;
animation-iteration-count: 1;
animation-fill-mode: forwards;
animation-timing-function: ease-out;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
sl-skeleton {
--border-radius: 8px;
height: 24px;
--color: var(--outline-color);
--sheen-color: var(--outline-hover-color);
}
sl-skeleton:nth-child(2) {
width: 70%;
}
sl-skeleton:nth-child(3) {
width: 30%;
}
sl-skeleton:nth-child(4) {
width: 90%;
}
`,
];
}
+51 -26
View File
@@ -8,9 +8,10 @@ import type {
Map,
Marker,
Polyline,
MarkerClusterGroup,
} from "leaflet";
import type { PropertyValues } from "lit";
import { ReactiveElement, css } from "lit";
import { css, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { formatDateTime } from "../../common/datetime/format_date_time";
@@ -26,6 +27,7 @@ import type { HomeAssistant, ThemeMode } from "../../types";
import { isTouch } from "../../util/is_touch";
import "../ha-icon-button";
import "./ha-entity-marker";
import { DecoratedMarker } from "../../common/map/decorated_marker";
declare global {
// for fire event
@@ -84,6 +86,9 @@ export class HaMap extends ReactiveElement {
@property({ type: Number }) public zoom = 14;
@property({ attribute: "cluster-markers", type: Boolean })
public clusterMarkers = true;
@state() private _loaded = false;
public leafletMap?: Map;
@@ -96,10 +101,12 @@ export class HaMap extends ReactiveElement {
private _mapFocusItems: (Marker | Circle)[] = [];
private _mapZones: (Marker | Circle)[] = [];
private _mapZones: DecoratedMarker[] = [];
private _mapFocusZones: (Marker | Circle)[] = [];
private _mapCluster: MarkerClusterGroup | undefined;
private _mapPaths: (Polyline | CircleMarker)[] = [];
private _clickCount = 0;
@@ -151,6 +158,10 @@ export class HaMap extends ReactiveElement {
}
}
if (changedProps.has("clusterMarkers")) {
this._drawEntities();
}
if (changedProps.has("_loaded") || changedProps.has("paths")) {
this._drawPaths();
}
@@ -175,6 +186,7 @@ export class HaMap extends ReactiveElement {
) {
return;
}
this._updateMapStyle();
}
@@ -426,6 +438,11 @@ export class HaMap extends ReactiveElement {
this._mapFocusZones = [];
}
if (this._mapCluster) {
this._mapCluster.remove();
this._mapCluster = undefined;
}
if (!this.entities) {
return;
}
@@ -481,26 +498,24 @@ export class HaMap extends ReactiveElement {
iconHTML = el.outerHTML;
}
// create marker with the icon
this._mapZones.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className,
}),
interactive: this.interactiveZones,
title,
})
);
// create circle around it
const circle = Leaflet.circle([latitude, longitude], {
interactive: false,
color: passive ? passiveZoneColor : zoneColor,
radius,
});
this._mapZones.push(circle);
const marker = new DecoratedMarker([latitude, longitude], circle, {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className,
}),
interactive: this.interactiveZones,
title,
});
this._mapZones.push(marker);
if (
this.fitZones &&
(typeof entity === "string" || entity.focus !== false)
@@ -538,7 +553,7 @@ export class HaMap extends ReactiveElement {
}
// create marker with the icon
const marker = Leaflet.marker([latitude, longitude], {
const marker = new DecoratedMarker([latitude, longitude], undefined, {
icon: Leaflet.divIcon({
html: entityMarker,
iconSize: [48, 48],
@@ -546,24 +561,34 @@ export class HaMap extends ReactiveElement {
}),
title: title,
});
this._mapItems.push(marker);
if (typeof entity === "string" || entity.focus !== false) {
this._mapFocusItems.push(marker);
}
// create circle around if entity has accuracy
if (gpsAccuracy) {
this._mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: darkPrimaryColor,
radius: gpsAccuracy,
})
);
marker.decorationLayer = Leaflet.circle([latitude, longitude], {
interactive: false,
color: darkPrimaryColor,
radius: gpsAccuracy,
});
}
this._mapItems.push(marker);
}
if (this.clusterMarkers) {
this._mapCluster = Leaflet.markerClusterGroup({
showCoverageOnHover: false,
removeOutsideVisibleBounds: false,
maxClusterRadius: 40,
});
this._mapCluster.addLayers(this._mapItems);
map.addLayer(this._mapCluster);
} else {
this._mapItems.forEach((marker) => map.addLayer(marker));
}
this._mapItems.forEach((marker) => map.addLayer(marker));
this._mapZones.forEach((marker) => map.addLayer(marker));
}
+72 -19
View File
@@ -1,25 +1,81 @@
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import "../ha-icon";
import "../ha-svg-icon";
import { classMap } from "lit/directives/class-map";
export type TileIconImageStyle = "square" | "rounded-square" | "circle";
export const DEFAULT_TILE_ICON_BORDER_STYLE = "circle";
@customElement("ha-tile-icon")
export class HaTileIcon extends LitElement {
@property({ type: Boolean, reflect: true })
public interactive = false;
@property({ attribute: "border-style", type: String })
public imageStyle?: TileIconImageStyle;
@property({ attribute: false })
public imageUrl?: string;
protected render(): TemplateResult {
return html`
<div class="shape">
if (this.imageUrl) {
const imageStyle = this.imageStyle || DEFAULT_TILE_ICON_BORDER_STYLE;
return html`
<div class="container ${classMap({ [imageStyle]: this.imageUrl })}">
<img alt="" src=${this.imageUrl} />
</div>
<slot></slot>
`;
}
return html`
<div class="container ${this.interactive ? "background" : ""}">
<slot name="icon"></slot>
</div>
<slot></slot>
`;
}
static styles = css`
:host {
--tile-icon-color: var(--disabled-color);
--mdc-icon-size: 22px;
--tile-icon-opacity: 0.2;
--tile-icon-hover-opacity: 0.35;
--mdc-icon-size: 24px;
position: relative;
user-select: none;
transition: transform 180ms ease-in-out;
}
.shape::before {
:host([interactive]:active) {
transform: scale(1.2);
}
:host([interactive]:hover) {
--tile-icon-opacity: var(--tile-icon-hover-opacity);
}
.container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 18px;
overflow: hidden;
transition: box-shadow 180ms ease-in-out;
}
:host([interactive]:focus-visible) .container {
box-shadow: 0 0 0 2px var(--tile-icon-color);
}
.container.rounded-square {
border-radius: 8px;
}
.container.square {
border-radius: 0;
}
.container.background::before {
content: "";
position: absolute;
top: 0;
@@ -27,24 +83,21 @@ export class HaTileIcon extends LitElement {
height: 100%;
width: 100%;
background-color: var(--tile-icon-color);
transition: background-color 180ms ease-in-out;
opacity: 0.2;
transition:
background-color 180ms ease-in-out,
opacity 180ms ease-in-out;
opacity: var(--tile-icon-opacity);
}
.shape {
position: relative;
width: 36px;
height: 36px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: color 180ms ease-in-out;
overflow: hidden;
}
.shape ::slotted(*) {
.container ::slotted([slot="icon"]) {
display: flex;
color: var(--tile-icon-color);
transition: color 180ms ease-in-out;
pointer-events: none;
}
.container img {
width: 100%;
height: 100%;
object-fit: cover;
}
`;
}
-53
View File
@@ -1,53 +0,0 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
export type TileImageStyle = "square" | "rounded-square" | "circle";
@customElement("ha-tile-image")
export class HaTileImage extends LitElement {
@property({ attribute: false }) public imageUrl?: string;
@property({ attribute: false }) public imageAlt?: string;
@property({ attribute: false }) public imageStyle: TileImageStyle = "circle";
protected render() {
return html`
<div class="image ${this.imageStyle}">
${this.imageUrl
? html`<img alt=${ifDefined(this.imageAlt)} src=${this.imageUrl} />`
: nothing}
</div>
`;
}
static styles = css`
.image {
position: relative;
width: 36px;
height: 36px;
border-radius: 18px;
display: flex;
flex: none;
align-items: center;
justify-content: center;
overflow: hidden;
}
.image.rounded-square {
border-radius: 8%;
}
.image.square {
border-radius: 0;
}
.image img {
width: 100%;
height: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-tile-image": HaTileImage;
}
}
+8 -1
View File
@@ -12,6 +12,7 @@ import type { HomeAssistant } from "../types";
import { fileDownload } from "../util/file_download";
import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation";
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
import checkValidDate from "../common/datetime/check_valid_date";
import { handleFetchPromise } from "../util/hass-call-api";
@@ -130,7 +131,13 @@ export interface BackupContentExtended extends BackupContent, BackupData {}
export interface BackupInfo {
backups: BackupContent[];
backing_up: boolean;
agent_errors: Record<string, string>;
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
last_non_idle_event: ManagerStateEvent | null;
next_automatic_backup: string | null;
next_automatic_backup_additional: boolean;
state: BackupManagerState;
}
export interface BackupDetails {
+1
View File
@@ -73,6 +73,7 @@ export interface CloudWebhook {
interface CloudLoginBase {
hass: HomeAssistant;
email: string;
check_connection?: boolean;
}
export interface CloudLoginPassword extends CloudLoginBase {
+2 -2
View File
@@ -233,11 +233,11 @@ export const restoreBackup = async (
type: HassioBackupDetail["type"],
backupSlug: string,
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
useSnapshotUrl: boolean
useBackupUrl: boolean
): Promise<void> => {
await hass.callApi<HassioResponse<{ job_id: string }>>(
"POST",
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
`hassio/${useBackupUrl ? "backups" : "snapshots"}/${backupSlug}/restore/${type}`,
backupDetails
);
};
+33
View File
@@ -0,0 +1,33 @@
import type { HomeAssistant } from "../types";
import {
fetchFrontendUserData,
saveFrontendUserData,
subscribeFrontendUserData,
} from "./frontend";
export const SIDEBAR_PREFERENCES_KEY = "sidebar";
export interface SidebarPreferences {
panelOrder?: string[];
hiddenPanels?: string[];
}
declare global {
interface FrontendUserData {
sidebar?: SidebarPreferences;
}
}
export const fetchSidebarPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY);
export const saveSidebarPreferences = (
hass: HomeAssistant,
data: SidebarPreferences
) => saveFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY, data);
export const subscribeSidebarPreferences = (
hass: HomeAssistant,
callback: (sidebar?: SidebarPreferences | null) => void
) =>
subscribeFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY, callback);
@@ -85,6 +85,7 @@ class StepFlowCreateEntry extends LitElement {
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
)
) {
this.navigateToResult = false;
this._flowDone();
showVoiceAssistantSetupDialog(this, {
deviceId: devices[0].id,
+7 -1
View File
@@ -40,8 +40,13 @@ export class DialogEnterCode
@state() private _showClearButton = false;
@state() private _narrow = false;
public async showDialog(dialogParams: EnterCodeDialogParams): Promise<void> {
this._dialogParams = dialogParams;
this._narrow = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
await this.updateComplete;
}
@@ -96,7 +101,7 @@ export class DialogEnterCode
>
<ha-textfield
class="input"
dialogInitialFocus
?dialogInitialFocus=${!this._narrow}
id="code"
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
type="password"
@@ -134,6 +139,7 @@ export class DialogEnterCode
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
type="password"
inputmode="numeric"
?dialogInitialFocus=${!this._narrow}
></ha-textfield>
<div class="keypad">
${BUTTONS.map((value) =>
@@ -49,6 +49,8 @@ class LightRgbColorPicker extends LitElement {
@state() private _hsPickerValue?: [number, number];
@state() private _isInteracting?: boolean;
protected render() {
if (!this.stateObj) {
return nothing;
@@ -211,7 +213,10 @@ class LightRgbColorPicker extends LitElement {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("entityId") && !changedProps.has("hass")) {
if (
this._isInteracting ||
(!changedProps.has("entityId") && !changedProps.has("hass"))
) {
return;
}
@@ -219,10 +224,13 @@ class LightRgbColorPicker extends LitElement {
}
private _hsColorCursorMoved(ev: CustomEvent) {
if (!ev.detail.value) {
const color = ev.detail.value;
this._isInteracting = color !== undefined;
if (color === undefined) {
return;
}
this._hsPickerValue = ev.detail.value;
this._hsPickerValue = color;
this._throttleUpdateColor();
}
@@ -22,7 +22,6 @@ import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity_attributes";
declare global {
interface HASSDomEvents {
"color-changed": LightColor;
"color-hovered": LightColor | undefined;
}
}
@@ -54,6 +53,8 @@ class LightColorTempPicker extends LitElement {
@state() private _ctPickerValue?: number;
@state() private _isInteracting?: boolean;
protected render() {
if (!this.stateObj) {
return nothing;
@@ -113,7 +114,7 @@ class LightColorTempPicker extends LitElement {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("stateObj")) {
if (this._isInteracting || !changedProps.has("stateObj")) {
return;
}
@@ -123,16 +124,14 @@ class LightColorTempPicker extends LitElement {
private _ctColorCursorMoved(ev: CustomEvent) {
const ct = ev.detail.value;
this._isInteracting = ct !== undefined;
if (isNaN(ct) || this._ctPickerValue === ct) {
return;
}
this._ctPickerValue = ct;
fireEvent(this, "color-hovered", {
color_temp_kelvin: ct,
});
this._throttleUpdateColorTemp();
}
@@ -143,8 +142,6 @@ class LightColorTempPicker extends LitElement {
private _ctColorChanged(ev: CustomEvent) {
const ct = ev.detail.value;
fireEvent(this, "color-hovered", undefined);
if (isNaN(ct) || this._ctPickerValue === ct) {
return;
}
@@ -99,7 +99,12 @@ class MoreInfoSirenAdvancedControls extends LitElement {
this._stateObj.attributes.available_tones
).map(
([toneId, toneName]) => html`
<ha-list-item .value=${toneId}
<ha-list-item
.value=${Array.isArray(
this._stateObj!.attributes.available_tones
)
? toneName
: toneId}
>${toneName}</ha-list-item
>
`
@@ -179,7 +184,7 @@ class MoreInfoSirenAdvancedControls extends LitElement {
await this.hass.callService("siren", "turn_on", {
entity_id: this._stateObj!.entity_id,
tone: this._tone,
volume: this._volume,
volume_level: this._volume,
duration: this._duration,
});
}
+4
View File
@@ -251,6 +251,7 @@ export class QuickBar extends LitElement {
<mwc-list>
${this._opened
? html`<lit-virtualizer
tabindex="-1"
scroller
@keydown=${this._handleListItemKeyDown}
@rangechange=${this._handleRangeChanged}
@@ -326,6 +327,7 @@ export class QuickBar extends LitElement {
.twoline=${Boolean(item.area)}
.item=${item}
index=${ifDefined(index)}
tabindex="0"
>
<span>${item.primaryText}</span>
${item.area
@@ -346,6 +348,7 @@ export class QuickBar extends LitElement {
.item=${item}
index=${ifDefined(index)}
graphic="icon"
tabindex="0"
>
${item.iconPath
? html`
@@ -375,6 +378,7 @@ export class QuickBar extends LitElement {
index=${ifDefined(index)}
class="command-item"
hasMeta
tabindex="0"
>
<span>
<ha-label
@@ -10,6 +10,7 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { cloudLogin } from "../../../data/cloud";
import { showCloudAlreadyConnectedDialog } from "../../../panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected";
import type { HomeAssistant } from "../../../types";
import {
showAlertDialog,
@@ -25,6 +26,8 @@ export class CloudStepSignin extends LitElement {
@state() private _error?: string;
@state() private _checkConnection = true;
@query("#email", true) private _emailField!: HaTextField;
@query("#password", true) private _passwordField!: HaPasswordField;
@@ -115,6 +118,7 @@ export class CloudStepSignin extends LitElement {
hass: this.hass,
email: username,
...(code ? { code } : { password }),
check_connection: this._checkConnection,
});
} catch (err: any) {
const errCode = err && err.body && err.body.code;
@@ -139,6 +143,20 @@ export class CloudStepSignin extends LitElement {
}
}
if (errCode === "alreadyconnectederror") {
showCloudAlreadyConnectedDialog(this, {
details: JSON.parse(err.body.message),
logInHereAction: () => {
this._checkConnection = false;
doLogin(username);
},
closeDialog: () => {
this._requestInProgress = false;
},
});
return;
}
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
await doLogin(username.toLowerCase());
return;
@@ -1,5 +1,4 @@
import { consume } from "@lit-labs/context";
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import {
mdiAlertCircleCheck,
mdiArrowDown,
@@ -27,7 +26,9 @@ import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
@@ -240,89 +241,104 @@ export default class HaAutomationActionRow extends LitElement {
</div> `
: nothing}
<ha-button-menu
<ha-md-button-menu
slot="icons"
@action=${this._handleAction}
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
fixed
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon">
<ha-md-menu-item .clickAction=${this._runAction}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.run"
)}
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
</ha-md-menu-item>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._renameAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<li divider role="separator"></li>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._duplicateAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="graphic"
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-list-item>
</ha-md-menu-item>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._copyAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._cutAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-list-item
graphic="icon"
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
></ha-list-item>
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-list-item
graphic="icon"
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
></ha-list-item>
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-list-item graphic="icon" .disabled=${!this._uiModeAvailable}>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this._uiModeAvailable}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="graphic"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<li divider role="separator"></li>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.action.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
@@ -331,15 +347,15 @@ export default class HaAutomationActionRow extends LitElement {
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="graphic"
slot="start"
.path=${this.action.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-list-item>
<ha-list-item
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
graphic="icon"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
@@ -347,11 +363,11 @@ export default class HaAutomationActionRow extends LitElement {
)}
<ha-svg-icon
class="warning"
slot="graphic"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
</ha-md-menu-item>
</ha-md-button-menu>
<div
class=${classMap({
@@ -424,47 +440,6 @@ export default class HaAutomationActionRow extends LitElement {
}
}
private async _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._runAction();
break;
case 1:
await this._renameAction();
break;
case 2:
fireEvent(this, "duplicate");
break;
case 3:
this._setClipboard();
break;
case 4:
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
break;
case 5:
fireEvent(this, "move-up");
break;
case 6:
fireEvent(this, "move-down");
break;
case 7:
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
break;
case 8:
this._onDisable();
break;
case 9:
this._onDelete();
break;
}
}
private _setClipboard() {
this._clipboard = {
...this._clipboard,
@@ -472,16 +447,16 @@ export default class HaAutomationActionRow extends LitElement {
};
}
private _onDisable() {
private _onDisable = () => {
const enabled = !(this.action.enabled ?? true);
const value = { ...this.action, enabled };
fireEvent(this, "value-changed", { value });
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
}
};
private async _runAction() {
private _runAction = async () => {
const validated = await validateConfig(this.hass, {
actions: this.action,
});
@@ -513,9 +488,9 @@ export default class HaAutomationActionRow extends LitElement {
"ui.panel.config.automation.editor.actions.run_action_success"
),
});
}
};
private _onDelete() {
private _onDelete = () => {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm_title"
@@ -530,7 +505,7 @@ export default class HaAutomationActionRow extends LitElement {
fireEvent(this, "value-changed", { value: null });
},
});
}
};
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
@@ -561,7 +536,7 @@ export default class HaAutomationActionRow extends LitElement {
this._yamlMode = true;
}
private async _renameAction(): Promise<void> {
private _renameAction = async (): Promise<void> => {
const alias = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.change_alias"
@@ -598,7 +573,37 @@ export default class HaAutomationActionRow extends LitElement {
this._yamlEditor?.setValue(value);
}
}
}
};
private _duplicateAction = () => {
fireEvent(this, "duplicate");
};
private _copyAction = () => {
this._setClipboard();
};
private _cutAction = () => {
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
};
private _moveUp = () => {
fireEvent(this, "move-up");
};
private _moveDown = () => {
fireEvent(this, "move-down");
};
private _toggleYamlMode = () => {
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
};
public expand() {
this.updateComplete.then(() => {
@@ -610,7 +615,6 @@ export default class HaAutomationActionRow extends LitElement {
return [
haStyle,
css`
ha-button-menu,
ha-icon-button {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
@@ -649,18 +653,11 @@ export default class HaAutomationActionRow extends LitElement {
border-top-right-radius: var(--ha-card-border-radius, 12px);
border-top-left-radius: var(--ha-card-border-radius, 12px);
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
mwc-list-item.hidden {
display: none;
}
.warning ul {
margin: 4px 0;
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
`,
];
@@ -1,5 +1,4 @@
import { consume } from "@lit-labs/context";
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import {
mdiArrowDown,
mdiArrowUp,
@@ -24,11 +23,12 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
import type {
AutomationClipboard,
Condition,
@@ -141,12 +141,12 @@ export default class HaAutomationConditionRow extends LitElement {
<slot name="icons" slot="icons"></slot>
<ha-button-menu
<ha-md-button-menu
slot="icons"
@action=${this._handleAction}
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
fixed
positioning="fixed"
>
<ha-icon-button
slot="trigger"
@@ -155,76 +155,91 @@ export default class HaAutomationConditionRow extends LitElement {
>
</ha-icon-button>
<ha-list-item graphic="icon">
<ha-md-menu-item .clickAction=${this._testCondition}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)}
<ha-svg-icon slot="graphic" .path=${mdiFlask}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.rename"
)}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<li divider role="separator"></li>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._duplicateCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="graphic"
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-list-item>
</ha-md-menu-item>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._copyCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._cutCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-list-item
graphic="icon"
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
></ha-list-item>
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-list-item
graphic="icon"
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
></ha-list-item>
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-list-item graphic="icon" .disabled=${this._warnings}>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="graphic"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<li divider role="separator"></li>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.condition.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
@@ -233,15 +248,15 @@ export default class HaAutomationConditionRow extends LitElement {
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="graphic"
slot="start"
.path=${this.condition.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-list-item>
<ha-list-item
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
graphic="icon"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
@@ -249,11 +264,11 @@ export default class HaAutomationConditionRow extends LitElement {
)}
<ha-svg-icon
class="warning"
slot="graphic"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
</ha-md-menu-item>
</ha-md-button-menu>
<div
class=${classMap({
@@ -325,47 +340,6 @@ export default class HaAutomationConditionRow extends LitElement {
}
}
private async _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._testCondition();
break;
case 1:
await this._renameCondition();
break;
case 2:
fireEvent(this, "duplicate");
break;
case 3:
this._setClipboard();
break;
case 4:
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
break;
case 5:
fireEvent(this, "move-up");
break;
case 6:
fireEvent(this, "move-down");
break;
case 7:
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
break;
case 8:
this._onDisable();
break;
case 9:
this._onDelete();
break;
}
}
private _setClipboard() {
this._clipboard = {
...this._clipboard,
@@ -373,13 +347,13 @@ export default class HaAutomationConditionRow extends LitElement {
};
}
private _onDisable() {
private _onDisable = () => {
const enabled = !(this.condition.enabled ?? true);
const value = { ...this.condition, enabled };
fireEvent(this, "value-changed", { value });
}
};
private _onDelete() {
private _onDelete = () => {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.conditions.delete_confirm_title"
@@ -394,7 +368,7 @@ export default class HaAutomationConditionRow extends LitElement {
fireEvent(this, "value-changed", { value: null });
},
});
}
};
private _switchUiMode() {
this._warnings = undefined;
@@ -406,7 +380,7 @@ export default class HaAutomationConditionRow extends LitElement {
this._yamlMode = true;
}
private async _testCondition() {
private _testCondition = async () => {
if (this._testing) {
return;
}
@@ -461,9 +435,9 @@ export default class HaAutomationConditionRow extends LitElement {
this._testing = false;
}, 2500);
}
}
};
private async _renameCondition(): Promise<void> {
private _renameCondition = async (): Promise<void> => {
const alias = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.conditions.change_alias"
@@ -489,7 +463,37 @@ export default class HaAutomationConditionRow extends LitElement {
value,
});
}
}
};
private _duplicateCondition = () => {
fireEvent(this, "duplicate");
};
private _copyCondition = () => {
this._setClipboard();
};
private _cutCondition = () => {
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
};
private _moveUp = () => {
fireEvent(this, "move-up");
};
private _moveDown = () => {
fireEvent(this, "move-down");
};
private _toggleYamlMode = () => {
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
};
public expand() {
this.updateComplete.then(() => {
@@ -501,9 +505,6 @@ export default class HaAutomationConditionRow extends LitElement {
return [
haStyle,
css`
ha-button-menu {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.disabled {
opacity: 0.5;
pointer-events: none;
@@ -539,12 +540,6 @@ export default class HaAutomationConditionRow extends LitElement {
border-top-right-radius: var(--ha-card-border-radius, 12px);
border-top-left-radius: var(--ha-card-border-radius, 12px);
}
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-list-item.hidden {
display: none;
}
.testing {
position: absolute;
top: 0px;
@@ -571,8 +566,8 @@ export default class HaAutomationConditionRow extends LitElement {
.testing.pass {
background-color: var(--success-color);
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
`,
];
@@ -339,9 +339,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
sortable: true,
groupable: true,
hidden: narrow,
title: "",
type: "overflow",
label: this.hass.localize("ui.panel.config.automation.picker.state"),
title: this.hass.localize("ui.panel.config.automation.picker.state"),
template: (automation) => html`
<ha-entity-toggle
.stateObj=${automation}
@@ -1,5 +1,4 @@
import { consume } from "@lit-labs/context";
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import {
mdiArrowDown,
mdiArrowUp,
@@ -28,7 +27,9 @@ import { capitalizeFirstLetter } from "../../../../common/string/capitalize-firs
import { handleStructError } from "../../../../common/structs/handle-errors";
import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
@@ -169,12 +170,12 @@ export default class HaAutomationTriggerRow extends LitElement {
<slot name="icons" slot="icons"></slot>
<ha-button-menu
<ha-md-button-menu
slot="icons"
@action=${this._handleAction}
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
fixed
positioning="fixed"
>
<ha-icon-button
slot="trigger"
@@ -182,84 +183,93 @@ export default class HaAutomationTriggerRow extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item
graphic="icon"
<ha-md-menu-item
.clickAction=${this._renameTrigger}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-list-item
graphic="icon"
<ha-md-menu-item
.clickAction=${this._showTriggerId}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon slot="graphic" .path=${mdiIdentifier}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
</ha-md-menu-item>
<li divider role="separator"></li>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._duplicateTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate"
)}
<ha-svg-icon
slot="graphic"
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-list-item>
</ha-md-menu-item>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._copyTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
<ha-md-menu-item
.clickAction=${this._cutTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-list-item
graphic="icon"
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
></ha-list-item>
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-list-item
graphic="icon"
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
></ha-list-item>
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-list-item graphic="icon" .disabled=${!supported}>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!supported}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="graphic"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<li divider role="separator"></li>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-list-item
graphic="icon"
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled || type === "list"}
>
${"enabled" in this.trigger && this.trigger.enabled === false
@@ -270,16 +280,16 @@ export default class HaAutomationTriggerRow extends LitElement {
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="graphic"
slot="start"
.path=${"enabled" in this.trigger &&
this.trigger.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-list-item>
<ha-list-item
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._onDelete}
class="warning"
graphic="icon"
.disabled=${this.disabled}
>
${this.hass.localize(
@@ -287,11 +297,11 @@ export default class HaAutomationTriggerRow extends LitElement {
)}
<ha-svg-icon
class="warning"
slot="graphic"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
</ha-md-menu-item>
</ha-md-button-menu>
<div
class=${classMap({
@@ -464,48 +474,6 @@ export default class HaAutomationTriggerRow extends LitElement {
}
}
private async _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._renameTrigger();
break;
case 1:
this._requestShowId = true;
this.expand();
break;
case 2:
fireEvent(this, "duplicate");
break;
case 3:
this._setClipboard();
break;
case 4:
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
break;
case 5:
fireEvent(this, "move-up");
break;
case 6:
fireEvent(this, "move-down");
break;
case 7:
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
break;
case 8:
this._onDisable();
break;
case 9:
this._onDelete();
break;
}
}
private _setClipboard() {
this._clipboard = {
...this._clipboard,
@@ -513,7 +481,7 @@ export default class HaAutomationTriggerRow extends LitElement {
};
}
private _onDelete() {
private _onDelete = () => {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.triggers.delete_confirm_title"
@@ -528,9 +496,9 @@ export default class HaAutomationTriggerRow extends LitElement {
fireEvent(this, "value-changed", { value: null });
},
});
}
};
private _onDisable() {
private _onDisable = () => {
if (isTriggerList(this.trigger)) return;
const enabled = !(this.trigger.enabled ?? true);
const value = { ...this.trigger, enabled };
@@ -538,7 +506,7 @@ export default class HaAutomationTriggerRow extends LitElement {
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
}
};
private _idChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
@@ -605,7 +573,7 @@ export default class HaAutomationTriggerRow extends LitElement {
});
}
private async _renameTrigger(): Promise<void> {
private _renameTrigger = async (): Promise<void> => {
if (isTriggerList(this.trigger)) return;
const alias = await showPromptDialog(this, {
title: this.hass.localize(
@@ -636,7 +604,42 @@ export default class HaAutomationTriggerRow extends LitElement {
this._yamlEditor?.setValue(value);
}
}
}
};
private _showTriggerId = () => {
this._requestShowId = true;
this.expand();
};
private _duplicateTrigger = () => {
fireEvent(this, "duplicate");
};
private _copyTrigger = () => {
this._setClipboard();
};
private _cutTrigger = () => {
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
};
private _moveUp = () => {
fireEvent(this, "move-up");
};
private _moveDown = () => {
fireEvent(this, "move-down");
};
private _toggleYamlMode = () => {
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
};
public expand() {
this.updateComplete.then(() => {
@@ -648,9 +651,6 @@ export default class HaAutomationTriggerRow extends LitElement {
return [
haStyle,
css`
ha-button-menu {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.disabled {
opacity: 0.5;
pointer-events: none;
@@ -714,18 +714,12 @@ export default class HaAutomationTriggerRow extends LitElement {
background-color: var(--accent-color);
color: var(--text-accent-color, var(--text-primary-color));
}
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-list-item.hidden {
display: none;
}
ha-textfield {
display: block;
margin-bottom: 24px;
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
`,
];
@@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
@@ -22,7 +23,6 @@ import {
import type { CloudStatus } from "../../../../../data/cloud";
import type { HomeAssistant } from "../../../../../types";
import { brandsUrl } from "../../../../../util/brands-url";
import { navigate } from "../../../../../common/navigate";
const DEFAULT_AGENTS = [];
@@ -46,7 +46,7 @@ enum BackupScheduleTime {
}
interface RetentionData {
type: "copies" | "days";
type: "copies" | "days" | "forever";
value: number;
}
@@ -55,7 +55,7 @@ const RETENTION_PRESETS: Record<
RetentionData
> = {
copies_3: { type: "copies", value: 3 },
forever: { type: "days", value: 0 },
forever: { type: "forever", value: 0 },
};
const SCHEDULE_OPTIONS = [
@@ -79,7 +79,10 @@ const computeRetentionPreset = (
data: RetentionData
): RetentionPreset | undefined => {
for (const [key, value] of Object.entries(RETENTION_PRESETS)) {
if (value.type === data.type && value.value === data.value) {
if (
value.type === data.type &&
(value.type === RetentionPreset.FOREVER || value.value === data.value)
) {
return key as RetentionPreset;
}
}
@@ -92,7 +95,7 @@ interface FormData {
time?: string | null;
days: BackupDay[];
retention: {
type: "copies" | "days";
type: "copies" | "days" | "forever";
value: number;
};
}
@@ -142,7 +145,12 @@ class HaBackupConfigSchedule extends LitElement {
? config.schedule.days
: [],
retention: {
type: config.retention.days != null ? "days" : "copies",
type:
config.retention.days === null && config.retention.copies === null
? "forever"
: config.retention.days != null
? "days"
: "copies",
value: config.retention.days ?? config.retention.copies ?? 3,
},
};
@@ -160,9 +168,11 @@ class HaBackupConfigSchedule extends LitElement {
: [],
},
retention:
data.retention.type === "days"
? { days: data.retention.value, copies: null }
: { copies: data.retention.value, days: null },
data.retention.type === "forever"
? { days: null, copies: null }
: data.retention.type === "days"
? { days: data.retention.value, copies: null }
: { copies: data.retention.value, days: null },
};
fireEvent(this, "value-changed", { value: this.value });
@@ -481,9 +491,19 @@ class HaBackupConfigSchedule extends LitElement {
private _retentionPresetChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
const value = target.value as RetentionPreset;
let value = target.value as RetentionPreset;
// custom needs to have a type of days or copies, set it to default copies 3
if (
value === RetentionPreset.CUSTOM &&
this._retentionPreset === RetentionPreset.FOREVER
) {
this._retentionPreset = value;
value = RetentionPreset.COPIES_3;
} else {
this._retentionPreset = value;
}
this._retentionPreset = value;
if (value !== RetentionPreset.CUSTOM) {
const data = this._getData(this.value);
const retention = RETENTION_PRESETS[value];
@@ -493,7 +513,7 @@ class HaBackupConfigSchedule extends LitElement {
}
this._setData({
...data,
retention: RETENTION_PRESETS[value],
retention,
});
}
}
@@ -504,6 +524,7 @@ class HaBackupConfigSchedule extends LitElement {
const value = parseInt(target.value);
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
const data = this._getData(this.value);
target.value = clamped.toString();
this._setData({
...data,
retention: {
@@ -8,7 +8,7 @@ import {
mdiUpload,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@@ -27,6 +27,7 @@ import type {
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-circular-progress";
import "../../../components/ha-fab";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon";
@@ -460,7 +461,17 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
extended
@click=${this._newBackup}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
${backupInProgress
? html`<div slot="icon">
<ha-circular-progress
.size=${"small"}
indeterminate
></ha-circular-progress>
</div>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiPlus}
></ha-svg-icon>`}
</ha-fab>
`
: nothing}
@@ -605,7 +616,14 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
}
static get styles(): CSSResultGroup {
return haStyle;
return [
haStyle,
css`
ha-circular-progress {
--md-sys-color-primary: var(--mdc-theme-on-secondary);
}
`,
];
}
}
@@ -8,6 +8,7 @@ import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-circular-progress";
import "../../../components/ha-icon";
import "../../../components/ha-icon-next";
import "../../../components/ha-icon-overflow-menu";
@@ -17,8 +18,10 @@ import type {
BackupAgent,
BackupConfig,
BackupContent,
BackupInfo,
} from "../../../data/backup";
import {
computeBackupAgentName,
generateBackup,
generateBackupWithAutomaticSettings,
} from "../../../data/backup";
@@ -50,6 +53,8 @@ class HaConfigBackupOverview extends LitElement {
@property({ attribute: false }) public manager!: ManagerStateEvent;
@property({ attribute: false }) public info?: BackupInfo;
@property({ attribute: false }) public backups: BackupContent[] = [];
@property({ attribute: false }) public fetching = false;
@@ -151,6 +156,26 @@ class HaConfigBackupOverview extends LitElement {
</ha-list-item>
</ha-button-menu>
<div class="content">
${this.info && Object.keys(this.info.agent_errors).length
? html`${Object.entries(this.info.agent_errors).map(
([agentId, error]) =>
html`<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.backup.overview.agent_error",
{
name: computeBackupAgentName(
this.hass.localize,
agentId,
this.agents
),
}
)}
>
${error}
</ha-alert>`
)}`
: nothing}
${backupInProgress
? html`
<ha-backup-overview-progress
@@ -204,7 +229,14 @@ class HaConfigBackupOverview extends LitElement {
extended
@click=${this._newBackup}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
${backupInProgress
? html`<div slot="icon">
<ha-circular-progress
.size=${"small"}
indeterminate
></ha-circular-progress>
</div>`
: html`<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>`}
</ha-fab>
</hass-subpage>
`;
@@ -231,6 +263,9 @@ class HaConfigBackupOverview extends LitElement {
padding-left: 0;
padding-right: 0;
}
ha-circular-progress {
--md-sys-color-primary: var(--mdc-theme-on-secondary);
}
`,
];
}
@@ -1,4 +1,4 @@
import { mdiDotsVertical, mdiHarddisk } from "@mdi/js";
import { mdiDotsVertical, mdiHarddisk, mdiOpenInNew } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -28,6 +28,7 @@ import "./components/config/ha-backup-config-encryption-key";
import "./components/config/ha-backup-config-schedule";
import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule";
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
import { documentationUrl } from "../../../util/documentation-url";
@customElement("ha-config-backup-settings")
class HaConfigBackupSettings extends LitElement {
@@ -98,6 +99,8 @@ class HaConfigBackupSettings extends LitElement {
return nothing;
}
const supervisor = isComponentLoaded(this.hass, "hassio");
return html`
<hass-subpage
back-path="/config/backup"
@@ -105,7 +108,7 @@ class HaConfigBackupSettings extends LitElement {
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.backup.settings.header")}
>
${isComponentLoaded(this.hass, "hassio")
${supervisor
? html`
<ha-button-menu slot="toolbar-icon">
<ha-icon-button
@@ -203,6 +206,29 @@ class HaConfigBackupSettings extends LitElement {
`
: nothing}
</div>
<div class="card-actions">
<a
href=${documentationUrl(this.hass, "/integrations/#backup")}
target="_blank"
rel="noreferrer"
>
<ha-button>
<ha-svg-icon slot="icon" .path=${mdiOpenInNew}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.settings.locations.more_locations"
)}
</ha-button>
</a>
${supervisor
? html`<a href="/config/storage">
<ha-button>
${this.hass.localize(
"ui.panel.config.backup.settings.locations.manage_network_storage"
)}
</ha-button>
</a>`
: nothing}
</div>
</ha-card>
<ha-card>
<div class="card-header">
@@ -342,6 +368,9 @@ class HaConfigBackupSettings extends LitElement {
.card-content {
padding-bottom: 0;
}
a {
text-decoration: none;
}
`;
}
+5 -5
View File
@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import type {
BackupAgent,
BackupConfig,
BackupContent,
BackupInfo,
} from "../../../data/backup";
import {
compareAgents,
@@ -44,7 +44,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
@state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE;
@state() private _backups: BackupContent[] = [];
@state() private _info?: BackupInfo;
@state() private _agents: BackupAgent[] = [];
@@ -87,8 +87,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
}
private async _fetchBackupInfo() {
const info = await fetchBackupInfo(this.hass);
this._backups = info.backups;
this._info = await fetchBackupInfo(this.hass);
}
private async _fetchBackupConfig() {
@@ -134,7 +133,8 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
pageEl.narrow = this.narrow;
pageEl.cloudStatus = this.cloudStatus;
pageEl.manager = this._manager;
pageEl.backups = this._backups;
pageEl.info = this._info;
pageEl.backups = this._info?.backups || [];
pageEl.config = this._config;
pageEl.agents = this._agents;
pageEl.fetching = this._fetching;
@@ -1,17 +1,13 @@
import { mdiContentCopy, mdiEye, mdiEyeOff, mdiHelpCircle } from "@mdi/js";
import { mdiHelpCircle } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-formfield";
import "../../../../components/ha-radio";
import "../../../../components/ha-settings-row";
import "../../../../components/ha-switch";
import "../../../../components/ha-textfield";
import { formatDate } from "../../../../common/datetime/format_date";
import type { HaSwitch } from "../../../../components/ha-switch";
@@ -25,6 +21,7 @@ import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
import { obfuscateUrl } from "../../../../util/url";
import "../../../../components/ha-copy-textfield";
@customElement("cloud-remote-pref")
export class CloudRemotePref extends LitElement {
@@ -34,8 +31,6 @@ export class CloudRemotePref extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state() private _unmaskedUrl = false;
protected render() {
if (!this.cloudStatus) {
return nothing;
@@ -139,37 +134,13 @@ export class CloudRemotePref extends LitElement {
)}
</p>
`}
<div class="url-container">
<div class="textfield-container">
<ha-textfield
.value=${this._unmaskedUrl
? `https://${remote_domain}`
: obfuscateUrl(`https://${remote_domain}`)}
readonly
.suffix=${
// reserve some space for the icon.
html`<div style="width: 24px"></div>`
}
></ha-textfield>
<ha-icon-button
class="toggle-unmasked-url"
toggles
.label=${this.hass.localize(
`ui.panel.config.common.${this._unmaskedUrl ? "hide" : "show"}_url`
)}
@click=${this._toggleUnmaskedUrl}
.path=${this._unmaskedUrl ? mdiEyeOff : mdiEye}
></ha-icon-button>
</div>
<ha-button
.url=${`https://${remote_domain}`}
@click=${this._copyURL}
unelevated
>
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
${this.hass.localize("ui.panel.config.common.copy_link")}
</ha-button>
</div>
<ha-copy-textfield
.hass=${this.hass}
.value=${`https://${remote_domain}`}
.maskedValue=${obfuscateUrl(`https://${remote_domain}`)}
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
></ha-copy-textfield>
<ha-expansion-panel
outlined
@@ -234,10 +205,6 @@ export class CloudRemotePref extends LitElement {
});
}
private _toggleUnmaskedUrl(): void {
this._unmaskedUrl = !this._unmaskedUrl;
}
private async _toggleChanged(ev) {
const toggle = ev.target as HaSwitch;
@@ -268,14 +235,6 @@ export class CloudRemotePref extends LitElement {
}
}
private async _copyURL(ev): Promise<void> {
const url = ev.currentTarget.url;
await copyToClipboard(url);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static styles = css`
.preparing {
padding: 0 16px 16px;
@@ -335,30 +294,6 @@ export class CloudRemotePref extends LitElement {
display: block;
margin-bottom: 16px;
}
.url-container {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.textfield-container {
position: relative;
flex: 1;
}
.textfield-container ha-textfield {
display: block;
}
.toggle-unmasked-url {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
hr {
border: none;
height: 1px;
@@ -0,0 +1,171 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import { createCloseHeading } from "../../../../components/ha-dialog";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { CloudAlreadyConnectedParams as CloudAlreadyConnectedDialogParams } from "./show-dialog-cloud-already-connected";
import { obfuscateUrl } from "../../../../util/url";
@customElement("dialog-cloud-already-connected")
class DialogCloudAlreadyConnected extends LitElement {
public hass!: HomeAssistant;
@state() private _params?: CloudAlreadyConnectedDialogParams;
@state() private _obfuscateIp = true;
public showDialog(params: CloudAlreadyConnectedDialogParams) {
this._params = params;
}
public closeDialog() {
this._params?.closeDialog();
this._params = undefined;
this._obfuscateIp = true;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
const { details } = this._params;
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.heading"
)
)}
>
<div class="intro">
<span>
${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.description"
)}
</span>
<b>
${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.other_home_assistant"
)}
</b>
</div>
<div class="instance-details">
<div class="instance-detail">
<span>
${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.ip_address"
)}:
</span>
<div class="obfuscated">
<span>
${this._obfuscateIp
? obfuscateUrl(details.remote_ip_address)
: details.remote_ip_address}
</span>
<ha-icon-button
class="toggle-unmasked-url"
.label=${this.hass.localize(
`ui.panel.config.cloud.dialog_already_connected.obfuscated_ip.${this._obfuscateIp ? "hide" : "show"}`
)}
@click=${this._toggleObfuscateIp}
.path=${this._obfuscateIp ? mdiEye : mdiEyeOff}
></ha-icon-button>
</div>
</div>
<div class="instance-detail">
<span>
${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.connected_at"
)}:
</span>
<span>
${formatDateTime(
new Date(details.connected_at),
this.hass.locale,
this.hass.config
)}
</span>
</div>
</div>
<ha-alert
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.info_backups.title"
)}
>
${this.hass.localize(
"ui.panel.config.cloud.dialog_already_connected.info_backups.description"
)}
</ha-alert>
<ha-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass!.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._logInHere} slot="primaryAction">
${this.hass!.localize(
"ui.panel.config.cloud.dialog_already_connected.login_here"
)}
</ha-button>
</ha-dialog>
`;
}
private _toggleObfuscateIp() {
this._obfuscateIp = !this._obfuscateIp;
}
private _logInHere() {
this._params?.logInHereAction();
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 535px;
}
.intro b {
display: block;
margin-top: 16px;
}
.instance-details {
display: flex;
flex-direction: column;
margin-bottom: 16px;
}
.instance-detail {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.obfuscated {
align-items: center;
display: flex;
flex-direction: row;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-cloud-already-connected": DialogCloudAlreadyConnected;
}
}
@@ -0,0 +1,21 @@
import { fireEvent } from "../../../../common/dom/fire_event";
export interface CloudAlreadyConnectedParams {
details: {
remote_ip_address: string;
connected_at: string;
};
logInHereAction: () => void;
closeDialog: () => void;
}
export const showCloudAlreadyConnectedDialog = (
element: HTMLElement,
webhookDialogParams: CloudAlreadyConnectedParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-cloud-already-connected",
dialogImport: () => import("./dialog-cloud-already-connected"),
dialogParams: webhookDialogParams,
});
};
@@ -1,27 +1,23 @@
import "@material/mwc-button";
import { mdiContentCopy, mdiOpenInNew } from "@mdi/js";
import { mdiOpenInNew } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { query, state } from "lit/decorators";
import { state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { documentationUrl } from "../../../../util/documentation-url";
import { showToast } from "../../../../util/toast";
import type { WebhookDialogParams } from "./show-dialog-manage-cloudhook";
import "../../../../components/ha-copy-textfield";
export class DialogManageCloudhook extends LitElement {
protected hass?: HomeAssistant;
@state() private _params?: WebhookDialogParams;
@query("ha-textfield") _input!: HaTextField;
public showDialog(params: WebhookDialogParams) {
this._params = params;
}
@@ -82,21 +78,12 @@ export class DialogManageCloudhook extends LitElement {
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
</p>
<ha-textfield
.label=${this.hass!.localize(
"ui.panel.config.cloud.dialog_cloudhook.public_url"
)}
<ha-copy-textfield
.hass=${this.hass}
.value=${cloudhook.cloudhook_url}
iconTrailing
readOnly
@click=${this._focusInput}
>
<ha-icon-button
@click=${this._copyUrl}
slot="trailingIcon"
.path=${mdiContentCopy}
></ha-icon-button>
</ha-textfield>
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
></ha-copy-textfield>
</div>
<a
@@ -137,24 +124,6 @@ export class DialogManageCloudhook extends LitElement {
}
}
private _focusInput(ev) {
const inputElement = ev.currentTarget as HaTextField;
inputElement.select();
}
private async _copyUrl(ev): Promise<void> {
if (!this.hass) return;
ev.stopPropagation();
const inputElement = ev.target.parentElement as HaTextField;
inputElement.select();
const url = this.hass.hassUrl(inputElement.value);
await copyToClipboard(url);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -163,13 +132,6 @@ export class DialogManageCloudhook extends LitElement {
ha-dialog {
width: 650px;
}
ha-textfield {
display: block;
}
ha-textfield > ha-icon-button {
--mdc-icon-button-size: 24px;
--mdc-icon-size: 18px;
}
button.link {
color: var(--primary-color);
text-decoration: none;
@@ -28,6 +28,7 @@ import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
import { showCloudAlreadyConnectedDialog } from "../dialog-cloud-already-connected/show-dialog-cloud-already-connected";
@customElement("cloud-login")
export class CloudLogin extends LitElement {
@@ -47,6 +48,8 @@ export class CloudLogin extends LitElement {
@state() private _error?: string;
@state() private _checkConnection = true;
@query("#email", true) private _emailField!: HaTextField;
@query("#password", true) private _passwordField!: HaPasswordField;
@@ -244,6 +247,7 @@ export class CloudLogin extends LitElement {
hass: this.hass,
email: username,
...(code ? { code } : { password }),
check_connection: this._checkConnection,
});
this.email = "";
this._password = "";
@@ -283,6 +287,21 @@ export class CloudLogin extends LitElement {
return;
}
}
if (errCode === "alreadyconnectederror") {
showCloudAlreadyConnectedDialog(this, {
details: JSON.parse(err.body.message),
logInHereAction: () => {
this._checkConnection = false;
doLogin(username);
},
closeDialog: () => {
this._requestInProgress = false;
this.email = "";
this._password = "";
},
});
return;
}
if (errCode === "PasswordChangeRequired") {
showAlertDialog(this, {
title: this.hass.localize(
@@ -355,6 +355,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
showNarrow: true,
sortable: true,
filterable: true,
minWidth: "80px",
maxWidth: "80px",
template: (entry) =>
entry.unavailable ||
entry.disabled_by ||
@@ -346,9 +346,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
groupable: true,
},
editable: {
title: "",
label: localize("ui.panel.config.helpers.picker.headers.editable"),
title: localize("ui.panel.config.helpers.picker.headers.editable"),
type: "icon",
sortable: true,
minWidth: "88px",
maxWidth: "88px",
showNarrow: true,
template: (helper) => html`
${!helper.editable
@@ -185,6 +185,14 @@ class AddIntegrationDialog extends LitElement {
const yamlIntegrations: IntegrationListItem[] = [];
Object.entries(i).forEach(([domain, integration]) => {
if (
"integration_type" in integration &&
integration.integration_type === "hardware"
) {
// Ignore hardware integrations, they cannot be added via UI
return;
}
if (
"integration_type" in integration &&
(integration.config_flow ||
@@ -70,7 +70,7 @@ export class HaConfigFlowCard extends LitElement {
? html`<a
href=${this.flow.context.configuration_url.replace(
/^homeassistant:\/\//,
""
"/"
)}
rel="noreferrer"
target=${this.flow.context.configuration_url.startsWith(
@@ -76,6 +76,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
@state()
private _statistics?: ZWaveJSControllerStatisticsUpdatedMessage;
private _dialogOpen = false;
protected async firstUpdated() {
if (this.hass) {
await this._fetchData();
@@ -104,11 +106,17 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
}
),
subscribeS2Inclusion(this.hass, this.configEntryId, (message) => {
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId,
dsk: message.dsk,
onStop: () => setTimeout(() => this._fetchData(), 100),
});
if (!this._dialogOpen) {
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId,
dsk: message.dsk,
onStop: () => {
setTimeout(() => this._fetchData(), 100);
this._dialogOpen = false;
},
});
this._dialogOpen = true;
}
}),
];
}
@@ -570,11 +578,17 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
}
private async _addNodeClicked() {
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId!,
// refresh the data after the dialog is closed. add a small delay for the inclusion state to update
onStop: () => setTimeout(() => this._fetchData(), 100),
});
if (!this._dialogOpen) {
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId!,
// refresh the data after the dialog is closed. add a small delay for the inclusion state to update
onStop: () => {
setTimeout(() => this._fetchData(), 100);
this._dialogOpen = false;
},
});
this._dialogOpen = true;
}
}
private async _removeNodeClicked() {
@@ -153,7 +153,6 @@ class ConfigUrlForm extends LitElement {
? html`
<ha-icon-button
class="toggle-unmasked-url"
toggles
.label=${this.hass.localize(
`ui.panel.config.common.${this._unmaskedExternalUrl ? "hide" : "show"}_url`
)}
@@ -254,7 +253,6 @@ class ConfigUrlForm extends LitElement {
? html`
<ha-icon-button
class="toggle-unmasked-url"
toggles
.label=${this.hass.localize(
`ui.panel.config.common.${this._unmaskedInternalUrl ? "hide" : "show"}_url`
)}
@@ -706,7 +706,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
--data-table-border-width: 0;
}
:host(:not([narrow])) ha-data-table {
height: calc(100vh - 1px - var(--header-height));
height: calc(100vh - 1px - var(--header-height) - 48px);
display: block;
}
@@ -20,40 +20,31 @@ export class HuiCardFeatures extends LitElement {
return nothing;
}
return html`
<div class="container">
${this.features.map(
(feature) => html`
<hui-card-feature
.hass=${this.hass}
.stateObj=${this.stateObj}
.color=${this.color}
.feature=${feature}
></hui-card-feature>
`
)}
</div>
${this.features.map(
(feature) => html`
<hui-card-feature
.hass=${this.hass}
.stateObj=${this.stateObj}
.color=${this.color}
.feature=${feature}
></hui-card-feature>
`
)}
`;
}
static styles = css`
:host {
--feature-color: var(--state-icon-color);
--feature-padding: 12px;
--feature-height: 42px;
--feature-border-radius: 12px;
--feature-button-spacing: 12px;
position: relative;
width: 100%;
}
.container {
position: relative;
display: flex;
flex-direction: column;
padding: var(--feature-padding);
padding-top: 0px;
gap: var(--feature-padding);
gap: 12px;
width: 100%;
height: 100%;
box-sizing: border-box;
justify-content: space-evenly;
}
@@ -0,0 +1,134 @@
import { mdiRestore, mdiPlus, mdiMinus } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-control-select";
import { UNAVAILABLE } from "../../../data/entity";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import { COUNTER_ACTIONS, type CounterActionsCardFeatureConfig } from "./types";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-button";
export const supportsCounterActionsCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "counter";
};
interface CounterButton {
translationKey: string;
icon: string;
serviceName: string;
disabled: boolean;
}
export const COUNTER_ACTIONS_BUTTON: Record<
string,
(stateObj: HassEntity) => CounterButton
> = {
increment: (stateObj) => ({
translationKey: "increment",
icon: mdiPlus,
serviceName: "increment",
disabled: parseInt(stateObj.state) === stateObj.attributes.maximum,
}),
reset: () => ({
translationKey: "reset",
icon: mdiRestore,
serviceName: "reset",
disabled: false,
}),
decrement: (stateObj) => ({
translationKey: "decrement",
icon: mdiMinus,
serviceName: "decrement",
disabled: parseInt(stateObj.state) === stateObj.attributes.minimum,
}),
};
@customElement("hui-counter-actions-card-feature")
class HuiCounterActionsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() private _config?: CounterActionsCardFeatureConfig;
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import(
"../editor/config-elements/hui-counter-actions-card-feature-editor"
);
return document.createElement("hui-counter-actions-card-feature-editor");
}
static getStubConfig(): CounterActionsCardFeatureConfig {
return {
type: "counter-actions",
actions: COUNTER_ACTIONS.map((action) => action),
};
}
public setConfig(config: CounterActionsCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsCounterActionsCardFeature(this.stateObj)
) {
return null;
}
return html`
<ha-control-button-group>
${this._config?.actions
?.filter((action) => COUNTER_ACTIONS.includes(action))
.map((action) => {
const button = COUNTER_ACTIONS_BUTTON[action](this.stateObj!);
return html`
<ha-control-button
.entry=${button}
.label=${this.hass!.localize(
// @ts-ignore
`ui.card.counter.actions.${button.translationKey}`
)}
@click=${this._onActionTap}
.disabled=${button.disabled ||
this.stateObj?.state === UNAVAILABLE}
>
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
</ha-control-button>
`;
})}
</ha-control-button-group>
`;
}
private _onActionTap(ev): void {
ev.stopPropagation();
const entry = (ev.target! as any).entry as CounterButton;
this.hass!.callService("counter", entry.serviceName, {
entity_id: this.stateObj!.entity_id,
});
}
static styles = cardFeatureStyles;
}
declare global {
interface HTMLElementTagNameMap {
"hui-counter-actions-card-feature": HuiCounterActionsCardFeature;
}
}
@@ -0,0 +1,111 @@
import { mdiPowerOff, mdiPower } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select";
import { UNAVAILABLE } from "../../../data/entity";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type { ToggleCardFeatureConfig } from "./types";
import { showToast } from "../../../util/toast";
export const supportsToggleCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return ["switch", "input_boolean"].includes(domain);
};
@customElement("hui-toggle-card-feature")
class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() private _config?: ToggleCardFeatureConfig;
static getStubConfig(): ToggleCardFeatureConfig {
return {
type: "toggle",
};
}
public setConfig(config: ToggleCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsToggleCardFeature(this.stateObj)
) {
return null;
}
const color = stateColorCss(this.stateObj);
const options = ["on", "off"].map<ControlSelectOption>((entityState) => ({
value: entityState,
label: this.hass!.formatEntityState(this.stateObj!, entityState),
path: entityState === "on" ? mdiPower : mdiPowerOff,
}));
return html`
<ha-control-select
.options=${options}
.value=${this.stateObj.state}
@value-changed=${this._valueChanged}
hide-label
.ariaLabel=${this.hass.localize("ui.card.humidifier.state")}
style=${styleMap({
"--control-select-color": color,
})}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
</ha-control-select>
`;
}
private async _valueChanged(ev: CustomEvent) {
const newState = (ev.detail as any).value;
if (
newState === this.stateObj!.state &&
!this.stateObj!.attributes.assumed_state
)
return;
const service = newState === "on" ? "turn_on" : "turn_off";
const domain = computeDomain(this.stateObj!.entity_id);
try {
await this.hass!.callService(domain, service, {
entity_id: this.stateObj!.entity_id,
});
} catch (_err) {
showToast(this, {
message: this.hass!.localize("ui.notification_toast.action_failed", {
service: domain + "." + service,
}),
duration: 5000,
dismissable: true,
});
}
}
static styles = cardFeatureStyles;
}
declare global {
interface HTMLElementTagNameMap {
"hui-toggle-card-feature": HuiToggleCardFeature;
}
}
@@ -83,6 +83,15 @@ export interface ClimatePresetModesCardFeatureConfig {
preset_modes?: string[];
}
export const COUNTER_ACTIONS = ["increment", "reset", "decrement"] as const;
export type CounterActions = (typeof COUNTER_ACTIONS)[number];
export interface CounterActionsCardFeatureConfig {
type: "counter-actions";
actions?: CounterActions[];
}
export interface SelectOptionsCardFeatureConfig {
type: "select-options";
options?: string[];
@@ -101,6 +110,10 @@ export interface TargetTemperatureCardFeatureConfig {
type: "target-temperature";
}
export interface ToggleCardFeatureConfig {
type: "toggle";
}
export interface WaterHeaterOperationModesCardFeatureConfig {
type: "water-heater-operation-modes";
operation_modes?: OperationMode[];
@@ -152,6 +165,7 @@ export type LovelaceCardFeatureConfig =
| ClimateSwingHorizontalModesCardFeatureConfig
| ClimateHvacModesCardFeatureConfig
| ClimatePresetModesCardFeatureConfig
| CounterActionsCardFeatureConfig
| CoverOpenCloseCardFeatureConfig
| CoverPositionCardFeatureConfig
| CoverTiltPositionCardFeatureConfig
@@ -170,6 +184,7 @@ export type LovelaceCardFeatureConfig =
| SelectOptionsCardFeatureConfig
| TargetHumidityCardFeatureConfig
| TargetTemperatureCardFeatureConfig
| ToggleCardFeatureConfig
| UpdateActionsCardFeatureConfig
| VacuumCommandsCardFeatureConfig
| WaterHeaterOperationModesCardFeatureConfig;
@@ -327,17 +327,19 @@ export class HuiEnergyDevicesDetailGraphCard
);
const untrackedConsumption: BarSeriesOption["data"] = [];
Object.keys(consumptionData.total).forEach((time) => {
const ts = Number(time);
const value =
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
const dataPoint: number[] = [ts, value];
if (compare) {
dataPoint[2] = dataPoint[0];
dataPoint[0] = compareTransform(new Date(ts)).getTime();
}
untrackedConsumption.push(dataPoint);
});
Object.keys(consumptionData.total)
.sort((a, b) => Number(a) - Number(b))
.forEach((time) => {
const ts = Number(time);
const value =
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
const dataPoint: number[] = [ts, value];
if (compare) {
dataPoint[2] = dataPoint[0];
dataPoint[0] = compareTransform(new Date(ts)).getTime();
}
untrackedConsumption.push(dataPoint);
});
// random id to always add untracked at the end
const order = Date.now();
const dataset: BarSeriesOption = {
@@ -448,7 +450,15 @@ export class HuiEnergyDevicesDetailGraphCard
});
});
return sorted_devices
.map((device) => data.find((d) => (d.id as string).includes(device))!)
.map(
(device) =>
data.find((d) => {
const id = (d.id as string)
.replace(/^compare-/, "") // Remove compare- prefix
.replace(/-\d+$/, ""); // Remove numeric suffix
return id === device;
})!
)
.filter(Boolean);
}
@@ -291,20 +291,19 @@ export class HuiEnergyUsageGraphCard
true
)
);
} else {
// add empty dataset so compare bars are first
// `stack: usage` so it doesn't take up space yet
const firstId = statIds.from_grid?.[0] ?? "placeholder";
datasets.push({
id: "compare-" + firstId,
type: "bar",
stack: "usage",
data: [],
// @ts-expect-error
order: 0,
});
}
// add empty dataset so compare bars are first
// `stack: usage` so it doesn't take up space yet
datasets.push({
id: "compare-placeholder",
type: "bar",
stack: energyData.statsCompare ? "compare" : "usage",
data: [],
// @ts-expect-error
order: 0,
});
datasets.push(
...this._processDataSet(
energyData.stats,
@@ -256,6 +256,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
hui-card-features {
width: 100%;
flex: none;
padding: 0 12px 12px 12px;
}
`;
}
+37 -12
View File
@@ -1,4 +1,8 @@
import { mdiImageFilterCenterFocus } from "@mdi/js";
import {
mdiDotsHexagon,
mdiGoogleCirclesCommunities,
mdiImageFilterCenterFocus,
} from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { LatLngTuple } from "leaflet";
import type { PropertyValues } from "lit";
@@ -72,6 +76,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
@state() private _error?: { code: string; message: string };
@state() private _clusterMarkers = true;
private _subscribed?: Promise<(() => Promise<void>) | undefined>;
public setConfig(config: MapCardConfig): void {
@@ -170,18 +176,32 @@ class HuiMapCard extends LitElement implements LovelaceCard {
.autoFit=${this._config.auto_fit || false}
.fitZones=${this._config.fit_zones}
.themeMode=${themeMode}
.clusterMarkers=${this._clusterMarkers}
interactive-zones
render-passive
></ha-map>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.cards.map.reset_focus"
)}
.path=${mdiImageFilterCenterFocus}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._fitMap}
tabindex="0"
></ha-icon-button>
<div id="buttons">
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.cards.map.toggle_grouping"
)}
.path=${this._clusterMarkers
? mdiGoogleCirclesCommunities
: mdiDotsHexagon}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._toggleClusterMarkers}
tabindex="0"
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.cards.map.reset_focus"
)}
.path=${mdiImageFilterCenterFocus}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._fitMap}
tabindex="0"
></ha-icon-button>
</div>
</div>
</ha-card>
`;
@@ -320,6 +340,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
this._map?.fitMap();
}
private _toggleClusterMarkers() {
this._clusterMarkers = !this._clusterMarkers;
}
private _getColor(entityId: string): string {
let color = this._colorDict[entityId];
if (color) {
@@ -464,11 +488,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
overflow: hidden;
}
ha-icon-button {
#buttons {
position: absolute;
top: 75px;
left: 3px;
outline: none;
display: flex;
flex-direction: column;
}
#root {
+58 -14
View File
@@ -3,17 +3,21 @@ 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 { fireEvent } from "../../../common/dom/fire_event";
import hash from "object-hash";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-markdown";
import "../../../components/ha-alert";
import type { RenderTemplateResult } from "../../../data/ws-templates";
import { subscribeRenderTemplate } from "../../../data/ws-templates";
import type { HomeAssistant } from "../../../types";
import { CacheManager } from "../../../util/cache-manager";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { MarkdownCardConfig } from "./types";
const templateCache = new CacheManager<RenderTemplateResult>(1000);
@customElement("hui-markdown-card")
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
@@ -68,9 +72,32 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
this._tryConnect();
}
private _computeCacheKey() {
return hash(this._config);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._tryDisconnect();
if (this._config && this._templateResult) {
const key = this._computeCacheKey();
templateCache.set(key, this._templateResult);
}
}
protected willUpdate(_changedProperties: PropertyValues): void {
super.willUpdate(_changedProperties);
if (!this._config) {
return;
}
if (!this._templateResult) {
const key = this._computeCacheKey();
if (templateCache.has(key)) {
this._templateResult = templateCache.get(key);
}
}
}
protected render() {
@@ -80,17 +107,26 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
return html`
${this._error
? html`<ha-alert
alert-type=${this._errorLevel?.toLowerCase() || "error"}
>${this._error}</ha-alert
>`
? html`
<ha-alert
.alertType=${(this._errorLevel?.toLowerCase() as
| "error"
| "warning") || "error"}
>
${this._error}
</ha-alert>
`
: nothing}
<ha-card .header=${this._config.title}>
<ha-card
.header=${!this._config.text_only ? this._config.title : undefined}
class=${classMap({
"with-header": !!this._config.title,
"text-only": this._config.text_only ?? false,
})}
>
<ha-markdown
cache
breaks
class=${classMap({
"no-header": !this._config.title,
})}
.content=${this._templateResult?.result}
></ha-markdown>
</ha-card>
@@ -107,7 +143,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
this._tryConnect();
}
const shouldBeHidden =
this._templateResult &&
!!this._templateResult &&
this._config.show_empty === false &&
this._templateResult.result.length === 0;
if (shouldBeHidden !== this.hidden) {
@@ -200,11 +236,19 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
margin-bottom: 8px;
}
ha-markdown {
padding: 0 16px 16px;
padding: 16px;
word-wrap: break-word;
}
ha-markdown.no-header {
padding-top: 16px;
.with-header ha-markdown {
padding: 0 16px 16px;
}
.text-only {
background: none;
box-shadow: none;
border: none;
}
.text-only ha-markdown {
padding: 2px 4px;
}
`;
}
@@ -327,7 +327,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
);
const endDate = this._energyEnd;
try {
let unitClass;
let unitClass: string | undefined | null;
if (this._config!.unit && this._metadata) {
const metadata = Object.values(this._metadata).find(
(metaData) =>
@@ -248,6 +248,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
hui-card-features {
width: 100%;
flex: none;
padding: 0 12px 12px 12px;
}
`;
}
+91 -78
View File
@@ -18,8 +18,7 @@ import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
import "../../../components/tile/ha-tile-badge";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-image";
import type { TileImageStyle } from "../../../components/tile/ha-tile-image";
import type { TileIconImageStyle } from "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import { cameraUrlWithWidthHeight } from "../../../data/camera";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
@@ -36,7 +35,7 @@ import type {
LovelaceGridOptions,
} from "../types";
import { renderTileBadge } from "./tile/badges/tile-badge";
import type { ThermostatCardConfig, TileCardConfig } from "./types";
import type { TileCardConfig } from "./types";
export const getEntityDefaultTileIconAction = (entityId: string) => {
const domain = computeDomain(entityId);
@@ -44,10 +43,10 @@ export const getEntityDefaultTileIconAction = (entityId: string) => {
DOMAINS_TOGGLE.has(domain) ||
["button", "input_button", "scene"].includes(domain);
return supportsIconAction ? "toggle" : "more-info";
return supportsIconAction ? "toggle" : "none";
};
const DOMAIN_IMAGE_STYLE: Record<string, TileImageStyle> = {
const DOMAIN_IMAGE_SHAPE: Record<string, TileIconImageStyle> = {
update: "square",
media_player: "rounded-square",
};
@@ -84,7 +83,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
@state() private _config?: TileCardConfig;
public setConfig(config: ThermostatCardConfig): void {
public setConfig(config: TileCardConfig): void {
if (!config.entity) {
throw new Error("Specify an entity");
}
@@ -101,10 +100,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}
public getCardSize(): number {
const featuresPosition =
this._config && this._featurePosition(this._config);
const featuresCount = this._config?.features?.length || 0;
return (
1 +
(this._config?.vertical ? 1 : 0) +
(this._config?.features?.length || 0)
(featuresPosition === "inline" ? 0 : featuresCount)
);
}
@@ -112,9 +114,16 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
const columns = 6;
let min_columns = 6;
let rows = 1;
if (this._config?.features?.length) {
rows += this._config.features.length;
const featurePosition = this._config && this._featurePosition(this._config);
const featuresCount = this._config?.features?.length || 0;
if (featuresCount) {
if (featurePosition === "inline") {
min_columns = 12;
} else {
rows += featuresCount;
}
}
if (this._config?.vertical) {
rows++;
min_columns = 3;
@@ -196,7 +205,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}
);
get hasCardAction() {
private get _hasCardAction() {
return (
!this._config?.tap_action ||
hasAction(this._config?.tap_action) ||
@@ -205,12 +214,29 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
);
}
get hasIconAction() {
private get _hasIconAction() {
return (
!this._config?.icon_tap_action || hasAction(this._config?.icon_tap_action)
);
}
private _featurePosition = memoizeOne((config: TileCardConfig) => {
if (config.vertical) {
return "bottom";
}
return config.features_position || "bottom";
});
private _displayedFeatures = memoizeOne((config: TileCardConfig) => {
const features = config.features || [];
const featurePosition = this._featurePosition(config);
if (featurePosition === "inline") {
return features.slice(0, 1);
}
return features;
});
protected render() {
if (!this._config || !this.hass) {
return nothing;
@@ -224,14 +250,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
return html`
<ha-card>
<div class="content ${classMap(contentClasses)}">
<div class="icon-container">
<ha-tile-icon>
<ha-svg-icon .path=${mdiHelp}></ha-svg-icon>
</ha-tile-icon>
<ha-tile-icon>
<ha-svg-icon slot="icon" .path=${mdiHelp}></ha-svg-icon>
<ha-tile-badge class="not-found">
<ha-svg-icon .path=${mdiExclamationThick}></ha-svg-icon>
</ha-tile-badge>
</div>
</ha-tile-icon>
<ha-tile-info
.primary=${entityId}
secondary=${this.hass.localize("ui.card.tile.not_found")}
@@ -266,6 +290,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
? this._getImageUrl(stateObj)
: undefined;
const featurePosition = this._featurePosition(this._config);
const features = this._displayedFeatures(this._config);
const containerOrientationClass =
featurePosition === "inline" ? "horizontal" : "";
return html`
<ha-card style=${styleMap(style)} class=${classMap({ active })}>
<div
@@ -275,58 +305,49 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
role=${ifDefined(this.hasCardAction ? "button" : undefined)}
tabindex=${ifDefined(this.hasCardAction ? "0" : undefined)}
role=${ifDefined(this._hasCardAction ? "button" : undefined)}
tabindex=${ifDefined(this._hasCardAction ? "0" : undefined)}
aria-labelledby="info"
>
<ha-ripple .disabled=${!this.hasCardAction}></ha-ripple>
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
</div>
<div class="container">
<div class="container ${containerOrientationClass}">
<div class="content ${classMap(contentClasses)}">
<div
class="icon-container"
role=${ifDefined(this.hasIconAction ? "button" : undefined)}
tabindex=${ifDefined(this.hasIconAction ? "0" : undefined)}
<ha-tile-icon
role=${ifDefined(this._hasIconAction ? "button" : undefined)}
tabindex=${ifDefined(this._hasIconAction ? "0" : undefined)}
@action=${this._handleIconAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.icon_hold_action),
hasDoubleClick: hasAction(this._config!.icon_double_tap_action),
})}
.interactive=${this._hasIconAction}
.imageStyle=${DOMAIN_IMAGE_SHAPE[domain]}
.imageUrl=${imageUrl}
data-domain=${ifDefined(domain)}
data-state=${ifDefined(stateObj?.state)}
>
${imageUrl
? html`
<ha-tile-image
.imageStyle=${DOMAIN_IMAGE_STYLE[domain] || "circle"}
.imageUrl=${imageUrl}
></ha-tile-image>
`
: html`
<ha-tile-icon
data-domain=${ifDefined(domain)}
data-state=${ifDefined(stateObj?.state)}
>
<ha-state-icon
.icon=${this._config.icon}
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
</ha-tile-icon>
`}
<ha-state-icon
slot="icon"
.icon=${this._config.icon}
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
${renderTileBadge(stateObj, this.hass)}
</div>
</ha-tile-icon>
<ha-tile-info
id="info"
.primary=${name}
.secondary=${stateDisplay}
></ha-tile-info>
</div>
${this._config.features
${features.length > 0
? html`
<hui-card-features
.hass=${this.hass}
.stateObj=${stateObj}
.color=${this._config.color}
.features=${this._config.features}
.features=${features}
></hui-card-features>
`
: nothing}
@@ -363,6 +384,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}
[role="button"] {
cursor: pointer;
pointer-events: auto;
}
[role="button"]:focus {
outline: none;
@@ -383,6 +405,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
flex-direction: column;
flex: 1;
}
.container.horizontal {
flex-direction: row;
}
.content {
position: relative;
display: flex;
@@ -390,55 +416,34 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
align-items: center;
padding: 10px;
flex: 1;
min-width: 0;
box-sizing: border-box;
pointer-events: none;
gap: 10px;
}
.vertical {
flex-direction: column;
text-align: center;
justify-content: center;
}
.vertical .icon-container {
margin-bottom: 10px;
margin-right: 0;
margin-inline-start: initial;
margin-inline-end: initial;
}
.vertical ha-tile-info {
width: 100%;
flex: none;
}
.icon-container {
position: relative;
flex: none;
margin-right: 10px;
margin-inline-start: initial;
margin-inline-end: 10px;
direction: var(--direction);
transition: transform 180ms ease-in-out;
}
.icon-container ha-tile-icon,
.icon-container ha-tile-image {
ha-tile-icon {
--tile-icon-color: var(--tile-color);
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
position: relative;
padding: 6px;
margin: -6px;
}
.icon-container ha-tile-badge {
ha-tile-badge {
position: absolute;
top: -3px;
right: -3px;
inset-inline-end: -3px;
top: 3px;
right: 3px;
inset-inline-end: 3px;
inset-inline-start: initial;
}
.icon-container[role="button"] {
pointer-events: auto;
}
.icon-container[role="button"]:focus-visible,
.icon-container[role="button"]:active {
transform: scale(1.2);
}
ha-tile-info {
position: relative;
min-width: 0;
@@ -447,6 +452,14 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}
hui-card-features {
--feature-color: var(--tile-color);
padding: 0 12px 12px 12px;
}
.container.horizontal hui-card-features {
width: calc(50% - var(--column-gap, 0px) / 2 - 12px);
flex: none;
--feature-height: 36px;
padding: 0 12px;
padding-inline-start: 0;
}
ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"],
@@ -37,6 +37,7 @@ import type {
LovelaceGridOptions,
} from "../types";
import type { WeatherForecastCardConfig } from "./types";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@customElement("hui-weather-forecast-card")
class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
@@ -106,7 +107,9 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
!this.isConnected ||
!this.hass ||
!this._config ||
!this._needForecastSubscription()
!this._needForecastSubscription() ||
!isComponentLoaded(this.hass, "weather") ||
!this.hass.states[this._config!.entity]
) {
return;
}
@@ -118,7 +121,14 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
(event) => {
this._forecastEvent = event;
}
);
).catch((e) => {
if (e.code === "invalid_entity_id") {
setTimeout(() => {
this._subscribed = undefined;
}, 2000);
}
throw e;
});
}
public connectedCallback(): void {
+2
View File
@@ -336,6 +336,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
export interface MarkdownCardConfig extends LovelaceCardConfig {
type: "markdown";
content: string;
text_only?: boolean;
title?: string;
card_size?: number;
entity_ids?: string | string[];
@@ -533,6 +534,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
icon_hold_action?: ActionConfig;
icon_double_tap_action?: ActionConfig;
features?: LovelaceCardFeatureConfig[];
features_position?: "bottom" | "inline";
}
export interface HeadingCardConfig extends LovelaceCardConfig {
@@ -23,8 +23,10 @@ import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { addCard } from "../editor/config-util";
import type { LovelaceCardPath } from "../editor/lovelace-path";
import {
findLovelaceContainer,
findLovelaceItems,
getLovelaceContainerPath,
parseLovelaceCardPath,
@@ -253,14 +255,24 @@ export class HuiCardEditMode extends LitElement {
}
private _duplicateCard(): void {
const { cardIndex } = parseLovelaceCardPath(this.path!);
const { cardIndex, sectionIndex } = parseLovelaceCardPath(this.path!);
const containerPath = getLovelaceContainerPath(this.path!);
const sectionConfig =
sectionIndex !== undefined
? findLovelaceContainer(this.lovelace!.config, containerPath)
: undefined;
const cardConfig = this._cards![cardIndex];
showEditCardDialog(this, {
lovelaceConfig: this.lovelace!.config,
saveConfig: this.lovelace!.saveConfig,
path: containerPath,
saveCardConfig: async (config) => {
const newConfig = addCard(this.lovelace!.config, containerPath, config);
await this.lovelace!.saveConfig(newConfig);
},
cardConfig,
sectionConfig,
isNew: true,
});
}
@@ -278,9 +278,12 @@ export class HuiCardOptions extends LitElement {
const cardConfig = this._cards![cardIndex];
showEditCardDialog(this, {
lovelaceConfig: this.lovelace!.config,
saveConfig: this.lovelace!.saveConfig,
path: containerPath,
saveCardConfig: async (config) => {
const newConfig = addCard(this.lovelace!.config, containerPath, config);
await this.lovelace!.saveConfig(newConfig);
},
cardConfig,
isNew: true,
});
}
@@ -18,6 +18,7 @@ import {
startOfWeek,
startOfYear,
subDays,
subMonths,
} from "date-fns";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
@@ -179,6 +180,30 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
calcDate(today, startOfYear, this.hass.locale, this.hass.config),
calcDate(today, endOfYear, this.hass.locale, this.hass.config),
],
[this.hass.localize("ui.components.date-range-picker.ranges.now-7d")]: [
calcDate(today, subDays, this.hass.locale, this.hass.config, 7),
calcDate(today, subDays, this.hass.locale, this.hass.config, 1),
],
[this.hass.localize("ui.components.date-range-picker.ranges.now-30d")]:
[
calcDate(today, subDays, this.hass.locale, this.hass.config, 30),
calcDate(today, subDays, this.hass.locale, this.hass.config, 1),
],
[this.hass.localize("ui.components.date-range-picker.ranges.now-12m")]:
[
calcDate(
subMonths(today, 12),
startOfMonth,
this.hass.locale,
this.hass.config
),
calcDate(
subMonths(today, 1),
endOfMonth,
this.hass.locale,
this.hass.config
),
],
};
}
}
@@ -248,6 +273,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
.ranges=${this._ranges}
@value-changed=${this._dateRangeChanged}
minimal
header-position
></ha-date-range-picker>
</div>
@@ -200,8 +200,6 @@ export class HuiGenericEntityRow extends LitElement {
padding-inline-start: 16px;
padding-inline-end: 8px;
flex: 1 1 30%;
min-height: 40px;
align-content: center;
}
.info,
.info > * {
@@ -235,8 +233,6 @@ export class HuiGenericEntityRow extends LitElement {
}
.value {
direction: ltr;
min-height: 40px;
align-content: center;
}
`;
}
@@ -4,6 +4,7 @@ import "../card-features/hui-climate-swing-modes-card-feature";
import "../card-features/hui-climate-swing-horizontal-modes-card-feature";
import "../card-features/hui-climate-hvac-modes-card-feature";
import "../card-features/hui-climate-preset-modes-card-feature";
import "../card-features/hui-counter-actions-card-feature";
import "../card-features/hui-cover-open-close-card-feature";
import "../card-features/hui-cover-position-card-feature";
import "../card-features/hui-cover-tilt-card-feature";
@@ -22,6 +23,7 @@ import "../card-features/hui-numeric-input-card-feature";
import "../card-features/hui-select-options-card-feature";
import "../card-features/hui-target-temperature-card-feature";
import "../card-features/hui-target-humidity-card-feature";
import "../card-features/hui-toggle-card-feature";
import "../card-features/hui-update-actions-card-feature";
import "../card-features/hui-vacuum-commands-card-feature";
import "../card-features/hui-water-heater-operation-modes-card-feature";
@@ -39,6 +41,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
"climate-swing-horizontal-modes",
"climate-hvac-modes",
"climate-preset-modes",
"counter-actions",
"cover-open-close",
"cover-position",
"cover-tilt-position",
@@ -57,6 +60,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
"select-options",
"target-humidity",
"target-temperature",
"toggle",
"update-actions",
"vacuum-commands",
"water-heater-operation-modes",
@@ -3,10 +3,10 @@ import "@material/mwc-tab/mwc-tab";
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import memoize from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
@@ -24,6 +24,7 @@ import {
computeCards,
computeSection,
} from "../../common/generate-lovelace-config";
import { addCard } from "../config-util";
import {
findLovelaceContainer,
parseLovelaceContainerPath,
@@ -241,11 +242,24 @@ export class HuiCreateDialogCard
}
}
const lovelaceConfig = this._params!.lovelaceConfig;
const containerPath = this._params!.path;
const saveConfig = this._params!.saveConfig;
const sectionConfig =
containerPath.length === 2
? findLovelaceContainer(lovelaceConfig, containerPath)
: undefined;
showEditCardDialog(this, {
lovelaceConfig: this._params!.lovelaceConfig,
saveConfig: this._params!.saveConfig,
path: this._params!.path,
lovelaceConfig,
saveCardConfig: async (newCardConfig) => {
const newConfig = addCard(lovelaceConfig, containerPath, newCardConfig);
await saveConfig(newConfig);
},
cardConfig: config,
sectionConfig,
isNew: true,
});
this.closeDialog();
@@ -13,7 +13,6 @@ import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import {
getCustomCardEntry,
isCustomType,
@@ -23,13 +22,12 @@ import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
import "../../cards/hui-card";
import "../../sections/hui-section";
import { addCard, replaceCard } from "../config-util";
import { getCardDocumentationURL } from "../get-dashboard-documentation-url";
import type { ConfigChangedEvent } from "../hui-element-editor";
import { findLovelaceContainer } from "../lovelace-path";
import type { GUIModeChangedEvent } from "../types";
import "./hui-card-element-editor";
import type { HuiCardElementEditor } from "./hui-card-element-editor";
@@ -59,9 +57,7 @@ export class HuiDialogEditCard
@state() private _cardConfig?: LovelaceCardConfig;
@state() private _containerConfig!:
| LovelaceViewConfig
| LovelaceSectionConfig;
@state() private _sectionConfig?: LovelaceSectionConfig;
@state() private _saving = false;
@@ -85,23 +81,10 @@ export class HuiDialogEditCard
this._GUImode = true;
this._guiModeAvailable = true;
const containerConfig = findLovelaceContainer(
params.lovelaceConfig,
params.path
);
this._sectionConfig = this._params.sectionConfig;
if ("strategy" in containerConfig) {
throw new Error("Can't edit strategy");
}
this._containerConfig = containerConfig;
if ("cardConfig" in params) {
this._cardConfig = params.cardConfig;
this._dirty = true;
} else {
this._cardConfig = this._containerConfig.cards?.[params.cardIndex];
}
this._cardConfig = params.cardConfig;
this._dirty = Boolean(this._params.isNew);
this.large = false;
if (this._cardConfig && !Object.isFrozen(this._cardConfig)) {
@@ -156,12 +139,12 @@ export class HuiDialogEditCard
};
protected render() {
if (!this._params) {
if (!this._params || !this._cardConfig) {
return nothing;
}
let heading: string;
if (this._cardConfig && this._cardConfig.type) {
if (this._cardConfig.type) {
let cardName: string | undefined;
if (isCustomType(this._cardConfig.type)) {
// prettier-ignore
@@ -181,13 +164,6 @@ export class HuiDialogEditCard
"ui.panel.lovelace.editor.edit_card.typed_header",
{ type: cardName }
);
} else if (!this._cardConfig) {
heading = this._containerConfig.title
? this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
{ name: this._containerConfig.title }
)
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
} else {
heading = this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.header"
@@ -230,10 +206,8 @@ export class HuiDialogEditCard
<div class="content">
<div class="element-editor">
<hui-card-element-editor
.showVisibilityTab=${this._cardConfig?.type !== "conditional"}
.sectionConfig=${this._isInSection
? this._containerConfig
: undefined}
.showVisibilityTab=${this._cardConfig.type !== "conditional"}
.sectionConfig=${this._sectionConfig}
.hass=${this.hass}
.lovelace=${this._params.lovelaceConfig}
.value=${this._cardConfig}
@@ -244,7 +218,7 @@ export class HuiDialogEditCard
></hui-card-element-editor>
</div>
<div class="element-preview">
${this._isInSection
${this._sectionConfig
? html`
<hui-section
.hass=${this.hass}
@@ -345,14 +319,10 @@ export class HuiDialogEditCard
this._cardEditorEl?.focusYamlEditor();
}
private get _isInSection() {
return this._params!.path.length === 2;
}
private _cardConfigInSection = memoizeOne(
(cardConfig?: LovelaceCardConfig) => {
(cardConfig: LovelaceCardConfig) => {
const { cards, title, ...containerConfig } = this
._containerConfig as LovelaceSectionConfig;
._sectionConfig as LovelaceSectionConfig;
return {
...containerConfig,
@@ -411,20 +381,18 @@ export class HuiDialogEditCard
return;
}
this._saving = true;
const path = this._params!.path;
await this._params!.saveConfig(
"cardConfig" in this._params!
? addCard(this._params!.lovelaceConfig, path, this._cardConfig!)
: replaceCard(
this._params!.lovelaceConfig,
[...path, this._params!.cardIndex],
this._cardConfig!
)
);
this._saving = false;
this._dirty = false;
showSaveSuccessToast(this, this.hass);
this.closeDialog();
try {
await this._params!.saveCardConfig(this._cardConfig!);
this._saving = false;
this._dirty = false;
showSaveSuccessToast(this, this.hass);
this.closeDialog();
} catch (err: any) {
showToast(this, {
message: err.message,
});
this._saving = false;
}
}
static get styles(): CSSResultGroup {
@@ -1,20 +1,15 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { LovelaceContainerPath } from "../lovelace-path";
export type EditCardDialogParams = {
export interface EditCardDialogParams {
lovelaceConfig: LovelaceConfig;
saveConfig: (config: LovelaceConfig) => void;
path: LovelaceContainerPath;
} & (
| {
cardIndex: number;
}
| {
cardConfig: LovelaceCardConfig;
}
);
saveCardConfig: (config: LovelaceCardConfig) => void;
cardConfig: LovelaceCardConfig;
sectionConfig?: LovelaceSectionConfig;
isNew?: boolean;
}
export const importEditCardDialog = () => import("./hui-dialog-edit-card");
@@ -24,6 +24,7 @@ import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-cli
import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-features/hui-climate-swing-horizontal-modes-card-feature";
import { supportsCounterActionsCardFeature } from "../../card-features/hui-counter-actions-card-feature";
import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature";
import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature";
import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature";
@@ -42,6 +43,7 @@ import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-feature";
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
@@ -58,6 +60,7 @@ const UI_FEATURE_TYPES = [
"climate-preset-modes",
"climate-swing-modes",
"climate-swing-horizontal-modes",
"counter-actions",
"cover-open-close",
"cover-position",
"cover-tilt-position",
@@ -76,6 +79,7 @@ const UI_FEATURE_TYPES = [
"select-options",
"target-humidity",
"target-temperature",
"toggle",
"update-actions",
"vacuum-commands",
"water-heater-operation-modes",
@@ -90,6 +94,7 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
"climate-preset-modes",
"climate-swing-modes",
"climate-swing-horizontal-modes",
"counter-actions",
"fan-preset-modes",
"humidifier-modes",
"lawn-mower-commands",
@@ -111,6 +116,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
supportsClimateSwingHorizontalModesCardFeature,
"climate-hvac-modes": supportsClimateHvacModesCardFeature,
"climate-preset-modes": supportsClimatePresetModesCardFeature,
"counter-actions": supportsCounterActionsCardFeature,
"cover-open-close": supportsCoverOpenCloseCardFeature,
"cover-position": supportsCoverPositionCardFeature,
"cover-tilt-position": supportsCoverTiltPositionCardFeature,
@@ -129,6 +135,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
"select-options": supportsSelectOptionsCardFeature,
"target-humidity": supportsTargetHumidityCardFeature,
"target-temperature": supportsTargetTemperatureCardFeature,
toggle: supportsToggleCardFeature,
"update-actions": supportsUpdateActionsCardFeature,
"vacuum-commands": supportsVacuumCommandsCardFeature,
"water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature,
@@ -0,0 +1,91 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import "../../../../components/ha-form/ha-form";
import type { HomeAssistant } from "../../../../types";
import {
COUNTER_ACTIONS,
type LovelaceCardFeatureContext,
type CounterActionsCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
@customElement("hui-counter-actions-card-feature-editor")
export class HuiCounterActionsCardFeatureEditor
extends LitElement
implements LovelaceCardFeatureEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: CounterActionsCardFeatureConfig;
public setConfig(config: CounterActionsCardFeatureConfig): void {
this._config = config;
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "actions",
selector: {
select: {
multiple: true,
mode: "list",
reorder: true,
options: COUNTER_ACTIONS.map((action) => ({
value: action,
label: `${localize(
`ui.panel.lovelace.editor.features.types.counter-actions.actions.${action}`
)}`,
})),
},
},
},
] as const
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const schema = this._schema(this.hass.localize);
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
};
}
declare global {
interface HTMLElementTagNameMap {
"hui-counter-actions-card-feature-editor": HuiCounterActionsCardFeatureEditor;
}
}
@@ -93,6 +93,7 @@ export class HuiEntityBadgeEditor
name: "color",
selector: {
ui_color: {
default_color: "state",
include_state: true,
},
},
@@ -1,29 +1,28 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { MarkdownCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { LocalizeFunc } from "../../../../common/translations/localize";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
text_only: optional(boolean()),
title: optional(string()),
content: string(),
theme: optional(string()),
})
);
const SCHEMA = [
{ name: "title", selector: { text: {} } },
{ name: "content", required: true, selector: { template: {} } },
{ name: "theme", selector: { theme: {} } },
] as const;
@customElement("hui-markdown-card-editor")
export class HuiMarkdownCardEditor
extends LitElement
@@ -38,16 +37,51 @@ export class HuiMarkdownCardEditor
this._config = config;
}
private _schema = memoizeOne(
(localize: LocalizeFunc, text_only: boolean) =>
[
{
name: "style",
required: true,
selector: {
select: {
mode: "dropdown",
options: ["card", "text-only"].map((style) => ({
label: localize(
`ui.panel.lovelace.editor.card.markdown.style_options.${style}`
),
value: style,
})),
},
},
},
...(!text_only
? ([{ name: "title", selector: { text: {} } }] as const)
: []),
{ name: "content", required: true, selector: { template: {} } },
] as const satisfies HaFormSchema[]
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const data = {
...this._config,
style: this._config.text_only ? "text-only" : "card",
};
const schema = this._schema(
this.hass.localize,
this._config.text_only || false
);
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
@@ -55,17 +89,23 @@ export class HuiMarkdownCardEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const config = { ...ev.detail.value };
if (config.style === "text-only") {
config.text_only = true;
} else {
delete config.text_only;
}
delete config.style;
fireEvent(this, "config-changed", { config });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "theme":
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
case "style":
case "content":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.markdown.${schema.name}`
@@ -21,6 +21,7 @@ import {
optional,
string,
} from "superstruct";
import { keyed } from "lit/directives/keyed";
import type {
HaFormSchema,
SchemaUnion,
@@ -84,6 +85,8 @@ export class HuiStackCardEditor
@state() protected _guiModeAvailable? = true;
protected _keys = new WeakMap<LovelaceCardConfig, string>();
protected _schema: readonly HaFormSchema[] = SCHEMA;
@query("hui-card-element-editor")
@@ -199,14 +202,16 @@ export class HuiStackCardEditor
@click=${this._handleDeleteCard}
></ha-icon-button>
</div>
<hui-card-element-editor
.hass=${this.hass}
.value=${this._config.cards[selected]}
.lovelace=${this.lovelace}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-card-element-editor>
${keyed(
this._getKey(this._config.cards[selected]),
html`<hui-card-element-editor
.hass=${this.hass}
.value=${this._config.cards[selected]}
.lovelace=${this.lovelace}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-card-element-editor>`
)}
`
: html`
<hui-card-picker
@@ -220,6 +225,14 @@ export class HuiStackCardEditor
`;
}
private _getKey(card: LovelaceCardConfig) {
if (!this._keys.has(card)) {
this._keys.set(card, Math.random().toString());
}
return this._keys.get(card)!;
}
protected _handleSelectedCard(ev) {
if (ev.target.id === "add-card") {
this._selectedCard = this._config!.cards.length;
@@ -236,7 +249,10 @@ export class HuiStackCardEditor
return;
}
const cards = [...this._config.cards];
cards[this._selectedCard] = ev.detail.config as LovelaceCardConfig;
const key = this._getKey(cards[this._selectedCard]);
const newCard = ev.detail.config as LovelaceCardConfig;
cards[this._selectedCard] = newCard;
this._keys.set(newCard, key);
this._config = { ...this._config, cards };
this._guiModeAvailable = ev.detail.guiModeAvailable;
fireEvent(this, "config-changed", { config: this._config });
@@ -8,6 +8,7 @@ import {
assert,
assign,
boolean,
enums,
object,
optional,
string,
@@ -15,6 +16,7 @@ import {
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
@@ -48,12 +50,25 @@ const cardConfigStruct = assign(
show_entity_picture: optional(boolean()),
vertical: optional(boolean()),
tap_action: optional(actionConfigStruct),
icon_tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
icon_tap_action: optional(actionConfigStruct),
icon_hold_action: optional(actionConfigStruct),
icon_double_tap_action: optional(actionConfigStruct),
features: optional(array(any())),
features_position: optional(enums(["bottom", "inline"])),
})
);
const ADVANCED_ACTIONS = [
"hold_action",
"icon_hold_action",
"double_tap_action",
"icon_double_tap_action",
] as const;
type AdvancedActions = (typeof ADVANCED_ACTIONS)[number];
@customElement("hui-tile-card-editor")
export class HuiTileCardEditor
extends LitElement
@@ -63,13 +78,46 @@ export class HuiTileCardEditor
@state() private _config?: TileCardConfig;
@state() private _displayActions?: AdvancedActions[];
public setConfig(config: TileCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
if (this._displayActions) return;
this._setDisplayActions(config);
}
private _setDisplayActions(config: TileCardConfig) {
this._displayActions = ADVANCED_ACTIONS.filter(
(action) => action in config
);
}
private _resetConfiguredActions() {
this._displayActions = undefined;
}
connectedCallback(): void {
super.connectedCallback();
if (this._config) {
this._setDisplayActions(this._config);
}
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._resetConfiguredActions();
}
private _schema = memoizeOne(
(entityId: string | undefined, hideState: boolean) =>
(
localize: LocalizeFunc,
entityId: string | undefined,
hideState: boolean,
vertical: boolean,
displayActions: AdvancedActions[] = []
) =>
[
{ name: "entity", selector: { entity: {} } },
{
@@ -105,12 +153,6 @@ export class HuiTileCardEditor
boolean: {},
},
},
{
name: "vertical",
selector: {
boolean: {},
},
},
{
name: "hide_state",
selector: {
@@ -132,6 +174,43 @@ export class HuiTileCardEditor
},
] as const satisfies readonly HaFormSchema[])
: []),
{
name: "",
type: "grid",
schema: [
{
name: "content_layout",
required: true,
selector: {
select: {
mode: "dropdown",
options: ["horizontal", "vertical"].map((value) => ({
label: localize(
`ui.panel.lovelace.editor.card.tile.content_layout_options.${value}`
),
value,
})),
},
},
},
{
name: "features_position",
required: true,
selector: {
select: {
mode: "dropdown",
options: ["bottom", "inline"].map((value) => ({
label: localize(
`ui.panel.lovelace.editor.card.tile.features_position_options.${value}`
),
value,
disabled: vertical && value === "inline",
})),
},
},
},
],
},
],
},
{
@@ -158,14 +237,14 @@ export class HuiTileCardEditor
},
},
},
{
name: "hold_action",
...displayActions.map((action) => ({
name: action,
selector: {
ui_action: {
default_action: "none",
default_action: "none" as const,
},
},
},
})),
],
},
] as const satisfies readonly HaFormSchema[]
@@ -179,9 +258,23 @@ export class HuiTileCardEditor
const entityId = this._config!.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
const schema = this._schema(entityId, this._config!.hide_state ?? false);
const schema = this._schema(
this.hass.localize,
entityId,
this._config.hide_state ?? false,
this._config.vertical ?? false,
this._displayActions
);
const data = this._config;
const data = {
...this._config,
content_layout: this._config.vertical ? "vertical" : "horizontal",
};
// Default features position to bottom and force it to bottom in vertical mode
if (!data.features_position || data.vertical) {
data.features_position = "bottom";
}
return html`
<ha-form
@@ -233,6 +326,12 @@ export class HuiTileCardEditor
delete config.state_content;
}
// Convert content_layout to vertical
if (config.content_layout) {
config.vertical = config.content_layout === "vertical";
delete config.content_layout;
}
fireEvent(this, "config-changed", { config });
}
@@ -287,12 +386,14 @@ export class HuiTileCardEditor
switch (schema.name) {
case "color":
case "icon_tap_action":
case "icon_hold_action":
case "icon_double_tap_action":
case "show_entity_picture":
case "vertical":
case "hide_state":
case "state_content":
case "content_layout":
case "appearance":
case "interactions":
case "features_position":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.${schema.name}`
);
@@ -328,6 +429,14 @@ export class HuiTileCardEditor
display: block;
margin-bottom: 24px;
}
.info {
color: var(--secondary-text-color);
margin-top: 0;
margin-bottom: 8px;
}
.features-form {
margin-bottom: 8px;
}
`,
];
}
+16 -3
View File
@@ -21,6 +21,7 @@ import {
import { createSectionElement } from "../create-element/create-section-element";
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { replaceCard } from "../editor/config-util";
import { performDeleteCard } from "../editor/delete-card";
import { parseLovelaceCardPath } from "../editor/lovelace-path";
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
@@ -253,11 +254,23 @@ export class HuiSection extends ReactiveElement {
ev.stopPropagation();
if (!this.lovelace) return;
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
const sectionConfig = this.config;
if (isStrategySection(sectionConfig)) {
return;
}
const cardConfig = sectionConfig.cards![cardIndex];
showEditCardDialog(this, {
lovelaceConfig: this.lovelace.config,
saveConfig: this.lovelace.saveConfig,
path: [this.viewIndex, this.index],
cardIndex,
saveCardConfig: async (newCardConfig) => {
const newConfig = replaceCard(
this.lovelace!.config,
[this.viewIndex, this.index, cardIndex],
newCardConfig
);
await this.lovelace!.saveConfig(newConfig);
},
sectionConfig,
cardConfig,
});
});
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
+15 -3
View File
@@ -21,6 +21,7 @@ import { showCreateBadgeDialog } from "../editor/badge-editor/show-create-badge-
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { replaceCard } from "../editor/config-util";
import {
type DeleteBadgeParams,
performDeleteBadge,
@@ -270,11 +271,22 @@ export class HUIView extends ReactiveElement {
});
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
const viewConfig = this.lovelace!.config.views[this.index];
if (isStrategyView(viewConfig)) {
return;
}
const cardConfig = viewConfig.cards![cardIndex];
showEditCardDialog(this, {
lovelaceConfig: this.lovelace.config,
saveConfig: this.lovelace.saveConfig,
path: [this.index],
cardIndex,
saveCardConfig: async (newCardConfig) => {
const newConfig = replaceCard(
this.lovelace!.config,
[this.index, cardIndex],
newCardConfig
);
await this.lovelace.saveConfig(newConfig);
},
cardConfig,
});
});
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
+123 -11
View File
@@ -1,10 +1,11 @@
import "@material/mwc-button";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-card";
import "../../components/ha-button";
import "../../components/ha-expansion-panel";
import "../../layouts/hass-tabs-subpage";
import { profileSections } from "./ha-panel-profile";
import { isExternal } from "../../data/external";
@@ -27,6 +28,9 @@ import "./ha-pick-time-zone-row";
import "./ha-push-notifications-row";
import "./ha-set-suspend-row";
import "./ha-set-vibrate-row";
import { storage } from "../../common/decorators/storage";
import type { HaSwitch } from "../../components/ha-switch";
import { fetchSidebarPreferences } from "../../data/sidebar";
@customElement("ha-profile-section-general")
class HaProfileSectionGeneral extends LitElement {
@@ -38,6 +42,20 @@ class HaProfileSectionGeneral extends LitElement {
@property({ attribute: false }) public route!: Route;
@storage({
key: "sidebarPanelOrder",
state: true,
subscribe: true,
})
private _devicePanelOrder?: string[];
@storage({
key: "sidebarHiddenPanels",
state: true,
subscribe: true,
})
private _deviceHiddenPanels?: string[];
private _unsubCoreData?: UnsubscribeFunc;
private _getCoreData() {
@@ -71,6 +89,9 @@ class HaProfileSectionGeneral extends LitElement {
}
protected render(): TemplateResult {
const deviceSidebarSettingsEnabled =
!!this._devicePanelOrder || !!this._deviceHiddenPanels;
return html`
<hass-tabs-subpage
main-page
@@ -91,9 +112,9 @@ class HaProfileSectionGeneral extends LitElement {
: ""}
</div>
<div class="card-actions">
<mwc-button class="warning" @click=${this._handleLogOut}>
<ha-button class="warning" @click=${this._handleLogOut}>
${this.hass.localize("ui.panel.profile.logout")}
</mwc-button>
</ha-button>
</div>
</ha-card>
<ha-card
@@ -128,6 +149,29 @@ class HaProfileSectionGeneral extends LitElement {
.narrow=${this.narrow}
.hass=${this.hass}
></ha-pick-first-weekday-row>
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.profile.customize_sidebar.header"
)}
</span>
<span
slot="description"
class=${deviceSidebarSettingsEnabled ? "device-info" : ""}
>
${this.hass.localize(
`ui.panel.profile.customize_sidebar.${!deviceSidebarSettingsEnabled ? "description" : "overwritten_by_device"}`
)}
</span>
<ha-button
.disabled=${deviceSidebarSettingsEnabled}
@click=${this._customizeSidebar}
>
${this.hass.localize(
"ui.panel.profile.customize_sidebar.button"
)}
</ha-button>
</ha-settings-row>
${this.hass.user!.is_admin
? html`
<ha-advanced-mode-row
@@ -159,20 +203,48 @@ class HaProfileSectionGeneral extends LitElement {
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.profile.customize_sidebar.header"
"ui.panel.profile.customize_sidebar.device_specific_header"
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.profile.customize_sidebar.description"
"ui.panel.profile.customize_sidebar.device_description"
)}
</span>
<mwc-button @click=${this._customizeSidebar}>
${this.hass.localize(
"ui.panel.profile.customize_sidebar.button"
)}
</mwc-button>
<ha-switch
.checked=${deviceSidebarSettingsEnabled}
@change=${this._toggleDeviceSidebarPreferences}
></ha-switch>
</ha-settings-row>
${deviceSidebarSettingsEnabled
? html`
<ha-expansion-panel
outlined
.header=${this.hass.localize(
"ui.panel.profile.customize_sidebar.device_specific_header"
)}
expanded
>
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.profile.customize_sidebar.header"
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.profile.customize_sidebar.description"
)}
</span>
<ha-button @click=${this._customizeSidebar}>
${this.hass.localize(
"ui.panel.profile.customize_sidebar.button"
)}
</ha-button>
</ha-settings-row>
</ha-expansion-panel>
`
: nothing}
${this.hass.dockedSidebar !== "auto" || !this.narrow
? html`
<ha-force-narrow-row
@@ -215,6 +287,38 @@ class HaProfileSectionGeneral extends LitElement {
fireEvent(this, "hass-edit-sidebar", { editMode: true });
}
private async _toggleDeviceSidebarPreferences(ev: Event) {
const switchElement = ev.target as HaSwitch;
const enabled = switchElement.checked;
if (!enabled) {
if (this._devicePanelOrder || this._deviceHiddenPanels) {
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.profile.customize_sidebar.delete_device_preferences_header"
),
text: this.hass.localize(
"ui.panel.profile.customize_sidebar.delete_device_preferences_description"
),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
});
if (confirm) {
this._devicePanelOrder = undefined;
this._deviceHiddenPanels = undefined;
} else {
// revert switch
switchElement.click();
}
}
} else {
const sidebarPreferences = await fetchSidebarPreferences(this.hass);
this._devicePanelOrder = sidebarPreferences?.panelOrder ?? [];
this._deviceHiddenPanels = sidebarPreferences?.hiddenPanels ?? [];
}
}
private _handleLogOut() {
showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.profile.logout_title"),
@@ -251,6 +355,14 @@ class HaProfileSectionGeneral extends LitElement {
text-align: center;
color: var(--secondary-text-color);
}
ha-expansion-panel {
margin: 0 8px 8px;
}
.device-info {
color: var(--warning-color);
}
`,
];
}
@@ -47,7 +47,12 @@ export class HaStateControlAlarmControlPanelModes extends LitElement {
}
private async _setMode(mode: AlarmMode) {
setProtectedAlarmControlPanelMode(this, this.hass!, this.stateObj!, mode);
await setProtectedAlarmControlPanelMode(
this,
this.hass!,
this.stateObj!,
mode
);
}
private async _valueChanged(ev: CustomEvent) {
+5 -2
View File
@@ -286,7 +286,10 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
clearInterval(this.__backendPingInterval);
this.__backendPingInterval = setInterval(() => {
if (this.hass?.connected) {
promiseTimeout(5000, this.hass?.connection.ping()).catch(() => {
// If the backend is busy, or the connection is latent,
// it can take more than 10 seconds for the ping to return.
// We give it a 15 second timeout to be safe.
promiseTimeout(15000, this.hass?.connection.ping()).catch(() => {
if (!this.hass?.connected) {
return;
}
@@ -296,7 +299,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
this.hass?.connection.reconnect(true);
});
}
}, 10000);
}, 30000);
}
protected hassReconnected() {

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