Compare commits

..

151 Commits

Author SHA1 Message Date
Paul Bottein
fff12acb6b Handle not existing panels in dashboard config (#28292) 2025-12-02 17:23:09 +01:00
Petar Petrov
3d327ed628 Update Energy dashboard layout (#28283) 2025-12-02 16:01:17 +01:00
Wendelin
0d51648de1 Use history to manage back button click in automations add TCA (#28289) 2025-12-02 15:43:13 +01:00
Wendelin
c5642c15b8 Automation add TCA: fix narrow subtitles & icons (#28291) 2025-12-02 14:17:55 +00:00
Paul Bottein
7f885010de Add dialog to reorder areas and floors (#28272) 2025-12-02 15:12:36 +01:00
Paul Bottein
356d51f974 Only show current weather in home overview (#28288) 2025-12-02 15:38:34 +02:00
Dave T
38a907e51e Separate action field YAML examples (#27218)
* Comma separate field examples if it is a list

* Remove prettier ignore and json.stringify all examples

* Use YAML format

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-02 13:34:25 +00:00
Bram Kragten
87c0b1d887 fix paste in add tca dialog (#28286) 2025-12-02 14:30:16 +01:00
Paul Bottein
0f195015b7 Fix container alignment in section view (#28287) 2025-12-02 15:29:11 +02:00
Petar Petrov
17a976af67 Fix index value for grid return in power sankey card (#28281) 2025-12-02 14:47:15 +02:00
Aidan Timson
a41a7e822a Remove placeholder for non device area picker in entity settings (#28285)
Remove placeholder for non device area picker
2025-12-02 11:52:02 +00:00
dependabot[bot]
5473bf56c6 Bump express from 4.21.2 to 4.22.1 (#28280)
Bumps [express](https://github.com/expressjs/express) from 4.21.2 to 4.22.1.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.2...v4.22.1)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 4.22.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 08:17:59 +02:00
karwosts
029eba7ab8 Safer lookup of description_placeholders when service is invalid (#28273) 2025-12-02 08:08:36 +02:00
Silas Krause
824a3f288d Revert custom markdown styles (#28277) 2025-12-02 08:07:45 +02:00
renovate[bot]
fdd89c05d3 Update dependency prettier to v3.7.2 (#28276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 08:06:30 +02:00
Paul Bottein
c33cb7fff9 Clean reference to floor compare (#28269)
Fix floor compare
2025-12-01 16:47:09 +02:00
Paul Bottein
de53ad8dce Add helper for floor level (#28268)
* Add helper for floor level

* Update src/translations/en.json

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-01 15:43:57 +01:00
Simon Lamon
2dec7490b6 Include background in light, climate and security views (#28264)
* Include background

* Remove background key

* Add imports
2025-12-01 16:20:50 +02:00
Aidan Timson
6ed4bd5ce8 Match more-info-update backup preferences (#28266) 2025-12-01 15:51:31 +02:00
Petar Petrov
4e899c56ed Reduce the duration of init animation for charts to 500ms (#28262)
Reduce the duration of init animation for charts
2025-12-01 15:43:20 +02:00
Petar Petrov
53c20a0493 Add power view and restructure energy dashboard layout (#28240) 2025-12-01 14:16:11 +01:00
Wendelin
be2d6e9212 Fix automation trigger ha icon (#28265) 2025-12-01 13:05:36 +00:00
Wendelin
915442c571 Respect system area sort in automation target tree (#28263) 2025-12-01 14:01:06 +01:00
Aidan Timson
de6ebc2d0a Add missing key for labs to show in quick bar (#28261) 2025-12-01 12:26:41 +00:00
Bram Kragten
bcb12fa062 Use name instead of description_configured for triggers and conditions (#28260) 2025-12-01 13:19:19 +01:00
Aidan Timson
078915743d Make labs toolbar icon use default color (#28255) 2025-12-01 12:08:25 +01:00
Petar Petrov
54c524127f Fix refresh in energy panel subviews (#28252) 2025-12-01 12:07:45 +01:00
Wendelin
334e1c35e1 Fix ha-bottom-sheet closed event (#28257) 2025-12-01 12:04:15 +01:00
Aidan Timson
528c7727e2 Fix 1px padding for subpage titles (#28256) 2025-12-01 12:03:38 +01:00
Aidan Timson
d1043e33df Fix subpage layout icon alignment (#28254) 2025-12-01 09:54:25 +00:00
dependabot[bot]
b088f2c0e5 Bump home-assistant/wheels from 2025.10.0 to 2025.11.0 (#28251)
Bumps [home-assistant/wheels](https://github.com/home-assistant/wheels) from 2025.10.0 to 2025.11.0.
- [Release notes](https://github.com/home-assistant/wheels/releases)
- [Commits](https://github.com/home-assistant/wheels/compare/2025.10.0...2025.11.0)

---
updated-dependencies:
- dependency-name: home-assistant/wheels
  dependency-version: 2025.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 08:53:24 +02:00
dependabot[bot]
b33f407493 Bump relative-ci/agent-action from 3.2.0 to 3.2.1 (#28250)
Bumps [relative-ci/agent-action](https://github.com/relative-ci/agent-action) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/relative-ci/agent-action/releases)
- [Commits](feb19ddc69...c45aaa919e)

---
updated-dependencies:
- dependency-name: relative-ci/agent-action
  dependency-version: 3.2.1
  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-12-01 08:48:44 +02:00
dependabot[bot]
502d76b316 Bump actions/setup-python from 6.0.0 to 6.1.0 (#28249)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](e797f83bcb...83679a892e)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 08:48:24 +02:00
dependabot[bot]
1ddc07c215 Bump softprops/action-gh-release from 2.4.2 to 2.5.0 (#28248)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.4.2 to 2.5.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](5be0e66d93...a06a81a03e)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 08:47:32 +02:00
dependabot[bot]
a611a5fc4e Bump github/codeql-action from 4.31.4 to 4.31.5 (#28247)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.4 to 4.31.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](e12f017898...fdbfb4d275)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.5
  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-12-01 08:46:51 +02:00
eringerli
c13bece6d0 fix stacking of multiple power sources (#28243) 2025-12-01 06:25:47 +00:00
Paulus Schoutsen
28a89ff9e6 Add custom element decorators instead of customElements.define (#28235)
* Add custom element decorators instead of customElements.define

* Ignore

* prettier
2025-12-01 06:38:31 +01:00
karwosts
81b5ddec9d Add water devices to energy data download (#28242) 2025-11-30 16:00:14 +01:00
renovate[bot]
ce86aabe32 Update dependency prettier to v3.7.1 (#28239)
* Update dependency prettier to v3.7.1

* format

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-30 13:33:41 +00:00
Silas Krause
a8910bcbe4 Fix markdown rendering for cached html (#28229)
* Render markdown table in wrapper.

* Fix markdown styles

* Fix formatting

* fix rendering for cache
2025-11-30 15:21:39 +02:00
ildar170975
0e4cf9f62d ha-picker-field: change left padding to align with other controls (#28217)
change --md-list-item-leading-space & --md-list-item-trailing-space
2025-11-29 14:00:27 +02:00
renovate[bot]
506635d649 Update vaadinWebComponents monorepo to v24.9.6 (#28225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-29 11:44:28 +00:00
karwosts
27e24ee49b Add missing helper to language selector (#28218) 2025-11-29 12:35:04 +01:00
renovate[bot]
bcd712b48c Update vitest monorepo to v4.0.14 (#28215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 20:23:38 +01:00
renovate[bot]
461ef9b916 Update dependency @rspack/core to v1.6.5 (#28207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 11:14:57 +01:00
renovate[bot]
b794989daa Update dependency @bundle-stats/plugin-webpack-filter to v4.21.7 (#28205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 08:29:33 +02:00
Silas Krause
6727ffaae0 Fix markdown styles regression (#28202)
* Render markdown table in wrapper.

* Fix markdown styles

* Fix formatting
2025-11-28 08:28:57 +02:00
Paul Bottein
4df8501b20 Fix ha icon size (#28201) 2025-11-27 22:54:44 +01:00
Aidan Timson
539d0e443f Fix ha-wa-dialog fullscreen and make alerts not fullscreen (#28175) 2025-11-27 22:01:30 +01:00
renovate[bot]
9c9d274b5c Update dependency typescript-eslint to v8.48.0 (#28196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 19:20:07 +01:00
Paul Bottein
d6e6bc0e80 Fix safe area for sidebar section views in Android (#28194) 2025-11-27 19:46:56 +02:00
ildar170975
530a70b168 Automations, scripts, scenes: add a tooltip for relative time (#28158)
* add a tooltip for last_triggered

* add a tooltip for last_triggered

* add a tooltip for last_activated

* Apply suggestions from code review

* Apply suggestion from @MindFreeze

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

* Apply suggestion from @MindFreeze

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

* Apply suggestion from @MindFreeze

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-27 19:44:11 +02:00
Wendelin
23137500f8 Add TCA by target sort like item collections (#28192) 2025-11-27 17:03:27 +01:00
Wendelin
63e7ed21a4 Fix lab automations icons and sidebar width (#28184)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-27 16:57:42 +01:00
Petar Petrov
92611f46f4 Fix water sankey calculation to include total supply from sources (#28191) 2025-11-27 16:44:56 +01:00
Bram Kragten
9bc896241d Always store token when using develop and serve (#28179) 2025-11-27 16:43:36 +01:00
Paul Bottein
2baafe620c Add hint to reorder areas and floors (#28189) 2025-11-27 16:32:23 +01:00
Wendelin
ce52bbaf8c Show hidden entities in target tree (#28181)
* Show hidden entities in target tree

* Fix types
2025-11-27 15:44:50 +02:00
Wendelin
0b4b8d9082 "Add TCA" dialog desktop height to 800px (#28182) 2025-11-27 14:42:18 +01:00
Petar Petrov
bddbb773b7 Fix sankey chart resizing (#28180) 2025-11-27 15:41:46 +02:00
Paul Bottein
d52e1e8835 Use hui-root for panel energy (#28149)
* Use hui-root for panel energy

* Review feedback

* Set empty prefs
2025-11-27 15:35:36 +02:00
Petar Petrov
0a9dccfd19 Refactor power sankey hierarchy to handle devices with not power sensor (#28164) 2025-11-27 12:21:55 +01:00
Petar Petrov
bfd78670cc Disable axis pointer on the energy devices bar chart to fix refresh issues on touch devices (#28163) 2025-11-27 12:20:06 +01:00
Petar Petrov
11276af1a0 Handle grouping by floor and area in power sankey card (#28162) 2025-11-27 12:19:42 +01:00
Paul Bottein
d7be46c00b Fix box shadow for sidebar tabs (#28170) 2025-11-27 12:19:15 +01:00
Paul Bottein
94f32ce242 Fix disabled dashboard picker when no custom dashboard (#28172) 2025-11-27 12:18:48 +01:00
Wendelin
ef3e8186bc Fix add condition default tab and blank styles (#28166) 2025-11-27 12:18:21 +01:00
Paul Bottein
50fcf622aa Fix labs back button (#28174) 2025-11-27 12:42:55 +02:00
Paul Bottein
77c2444be8 Restore sidebar view when clicking back (#28167) 2025-11-27 12:42:36 +02:00
Wendelin
e5cb26cd3d Fix automation add TCA autofocus (#28168)
Fix automation add tca autofocus
2025-11-27 11:28:18 +01:00
Simon Lamon
2896519bfd Don't show more info for untracked consumption (#28151) 2025-11-27 09:55:23 +02:00
ildar170975
0b6e35eb53 Data tables: make sorting direction 2-state instead of 3-state (#28160)
* sorting is 2-state

* sorting is 2-state

* sorting is 2-state
2025-11-27 09:54:38 +02:00
Iván Pereira
e80a855f87 Fix hide sidebar tooltip on touchend events (#28042)
* fix: hide sidebar tooltip on touchend events

* Add a comment recommended by Copilot

* Clear timeouts id in disconnectedCallback
2025-11-27 09:03:23 +02:00
dependabot[bot]
7c88cf4e30 Bump node-forge from 1.3.1 to 1.3.2 (#28157)
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.1 to 1.3.2.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 06:49:09 +01:00
Petar Petrov
9001cd3e65 Replace gauges with energy usage graph in energy overview (#28150) 2025-11-26 17:37:18 +01:00
renovate[bot]
ca8923d8f4 Update dependency glob to v13 (#28135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 18:13:05 +02:00
Bram Kragten
e7e4407a09 Merge branch 'rc' into dev 2025-11-26 16:10:48 +01:00
Wendelin
3f0c9538bd Remove SubscribeMixin from automation and Z-Wave JS dialog components (#28146) 2025-11-26 15:05:47 +00:00
Bram Kragten
5c3ccbfdad Remove hard coded mqtt trigger, and migrate to new format (#28143) 2025-11-26 14:56:26 +00:00
Bram Kragten
9710142c47 Resubscribe to descriptions when labs feat changes (#28145) 2025-11-26 15:54:31 +01:00
Petar Petrov
57640d17cd Add show_only_totals option to energy sources table (#28147) 2025-11-26 15:49:14 +01:00
Wendelin
b5d93e7515 Remove chains of new conditions (#28140) 2025-11-26 15:09:40 +01:00
Steven Travers
ca9b29d82a Show encryption key in actions for esphome device (#28080)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-11-26 13:43:58 +00:00
Bram Kragten
51efa4f61f Set defaults for platform triggers and conditions (#28138) 2025-11-26 14:05:39 +01:00
Bram Kragten
a6c71719d1 Only show section title when it has content (#28141) 2025-11-26 12:56:55 +00:00
Bram Kragten
ccc48d158a Bumped version to 20251105.1 2025-11-21 13:50:20 +01:00
Bram Kragten
b11e787f09 Dont add store token for external auth flows (#28026)
* Dont add store token for external auth flows

* Apply suggestion from @MindFreeze

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-21 13:50:04 +01:00
renovate[bot]
cdb6562de8 Update dependency js-yaml to v4.1.1 [SECURITY] (#27955)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-21 13:48:48 +01:00
karwosts
d8e8c9aa02 Fix media image on dashboard-level background (#27934) 2025-11-21 13:48:10 +01:00
Petar Petrov
be392be1e6 Increase ZHA reconfiguration dialog width for details view (#27909) 2025-11-21 13:44:41 +01:00
Petar Petrov
10dc432445 Fix entity name in statistics chart (#27896) 2025-11-21 13:44:40 +01:00
Wendelin
19187f887d Fix target picker for entity_id: none (#27893)
Fix notFound condition to exclude 'none' in ha-target-picker-item-row
2025-11-21 13:44:40 +01:00
Petar Petrov
dc76a42aaa Smooth sensor card more when "Show more detail" is disabled (#27891)
* Smooth sensor card more when "Show more detail" is disabled

* Set minimum sample points to 10
2025-11-21 13:44:38 +01:00
Wendelin
1f2b8047a6 Use ha-ripple in ha-md-list-item (#27889) 2025-11-21 13:44:38 +01:00
Petar Petrov
e8c9ed0528 Dynamic total energy for pie chart (#27883) 2025-11-21 13:44:37 +01:00
Petar Petrov
c7ae78c02f Fix chart label outline color (#27882) 2025-11-21 13:44:36 +01:00
karwosts
dc8f1211e6 Fix entity editor with non-existant entity (#27875) 2025-11-21 13:44:35 +01:00
Yuksel Beyti
5c25a63ea5 Fix malformed HTML tags in backup backups component (#27872) 2025-11-21 13:44:34 +01:00
Paul Bottein
c1787ab994 Fix backup download and delete actions (#27851) 2025-11-21 13:44:33 +01:00
Paul Bottein
6fea535fdc Fix OHF logo theme (#27830) 2025-11-21 13:44:32 +01:00
Wendelin
e8cee84380 Fix floor details area picker (#27827) 2025-11-21 13:44:31 +01:00
Wendelin
b4613edeb7 Target picker row check if not found entity isn't "all" (#27826)
Target picker row check if not found entity isn't all
2025-11-21 13:44:30 +01:00
Wendelin
a8b6e5aa3d Add trigger/condition/action dialog: select single search result with enter key (#27825)
* Add trigger/condition/action dialog: select single search result with enter key

* Update src/panels/config/automation/add-automation-element-dialog.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-21 13:44:29 +01:00
Jan Bouwhuis
e842193cd6 Fix index for service action translation in service action dialog (#27824) 2025-11-21 13:44:28 +01:00
Bram Kragten
bb0813333d Fix landing page build (#27817) 2025-11-21 13:44:27 +01:00
Petar Petrov
ab4c6f80f4 Disable graph resize animation for general resizing (#27816) 2025-11-21 13:44:26 +01:00
Bram Kragten
89796e425a Bumped version to 20251105.0 2025-11-05 15:26:35 +01:00
Wendelin
9c42c8bbc4 Add fallback icon for domain template (#27814) 2025-11-05 15:25:54 +01:00
Wendelin
616237caee Fix target picker with empty sections (#27813) 2025-11-05 15:25:53 +01:00
Wendelin
2d36a0d37f Add trigger/condition/action dialog - Show device group always on top (#27812)
add automation element dialog Device always on top
2025-11-05 15:25:52 +01:00
Wendelin
1ec432a20f Change add trigger/condition/action dialog title (#27811)
Change add dialog title
2025-11-05 15:25:51 +01:00
Wendelin
afd91b2261 Fix auth language picker styles (#27805) 2025-11-05 15:25:50 +01:00
Paul Bottein
cdfb7f914f Fix target picker in logbook card editor (#27804)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-11-05 15:24:49 +01:00
Wendelin
33b0897522 Add trigger/condition/action dialog: fix empty elements in search results (#27802) 2025-11-05 15:16:14 +01:00
Wendelin
5f0cf1b522 Add condition/action dialog: blocks title (#27801) 2025-11-05 15:16:13 +01:00
Wendelin
afb2ad95a4 Fix target picker in card editor (#27800) 2025-11-05 15:16:12 +01:00
Jan-Philipp Benecke
27beab3133 Fix z-index for target picker item row icon (#27798) 2025-11-05 15:16:10 +01:00
Wendelin
435c82489b Fix assist conversation language picker (#27764) 2025-11-05 15:16:09 +01:00
Bram Kragten
3ba6bf272e Bumped version to 20251104.0 2025-11-04 18:21:11 +01:00
Paul Bottein
eec99b2fa3 Rename safety panel to security panel (#27796) 2025-11-04 18:20:10 +01:00
Bram Kragten
d23e45e410 Handle unknown items in target picker (#27795)
* Handle unknown items in target picker

* Update ha-target-picker-item-row.ts

* update colors

* fallback to domain icons
2025-11-04 18:19:02 +01:00
Paul Bottein
3c82d12609 Auto refresh summary dashboard when registries changed (#27794) 2025-11-04 18:17:46 +01:00
Paul Bottein
15d67997e7 Don't show summary card if summary dashboards are empty (#27788)
Don't show summary card if summary dashboard are empty
2025-11-04 18:16:37 +01:00
Paul Bottein
a6dfcb3100 Fix tooltip hide delay (#27786) 2025-11-04 18:16:37 +01:00
karwosts
26c2369228 Fix sankey with external statistics devices (#27784) 2025-11-04 18:16:35 +01:00
Paul Bottein
2eed446492 Display entities without area in summary dashboard (#27777)
* Add support for no area, no floor and no device in entity filter

* Display entities without area in summary dashboard
2025-11-04 18:16:34 +01:00
Wendelin
7ebdeab6b2 Fix-labels-yaml-helper (#27776)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-11-04 18:16:33 +01:00
Tobias Bieniek
0c35278f51 Hide media players summary when no entities exist (#27642) 2025-11-04 18:16:32 +01:00
Bram Kragten
561122f03d Bumped version to 20251103.0 2025-11-03 16:34:11 +01:00
Petar Petrov
95311be034 Apply theme variables to pi charts (#27773) 2025-11-03 16:34:07 +01:00
Wendelin
1eda44a102 Fix selected element text color (#27771) 2025-11-03 16:32:51 +01:00
Petar Petrov
d76781eb91 Fix for Y axis label formatting in history graph (#27770) 2025-11-03 16:32:50 +01:00
Petar Petrov
82d44e051f Fix sensor card graph in Safari (#27768) 2025-11-03 16:32:49 +01:00
Aidan Timson
fdc9f5a3b7 Use supervisor endpoint for downloading logs (when avaliable) (#27765) 2025-11-03 16:32:47 +01:00
Paul Bottein
ee6c82aba9 Don't show tooltip on overflow menu in dashboard toolbar (#27763) 2025-11-03 16:32:46 +01:00
Paul Bottein
11d3f5c2ba Fix suggest cards dialog for sections view (#27762) 2025-11-03 16:32:45 +01:00
Aarni Koskela
feb68ce373 Add support for PM4 sensor state (#27754) 2025-11-03 16:32:44 +01:00
Simon Lamon
7f9a9de157 Move label translations to ui.dialog (#27752) 2025-11-03 16:32:43 +01:00
Simon Lamon
8e1b6a3d3b Fixes in backup overflow (#27745)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-11-03 16:32:41 +01:00
Jan-Philipp Benecke
6e6e5a53e2 Fix button text overflow (#27744) 2025-11-03 16:32:41 +01:00
Bram Kragten
0408734ec5 Bumped version to 20251029.1 2025-10-30 18:19:31 +01:00
Paul Bottein
317519fc08 Revert "Fix entities card size and add grid contstraints" (#27725) 2025-10-30 18:18:27 +01:00
Paul Bottein
843d79eab4 Don't show tooltip for ha button menu in top bar (#27723) 2025-10-30 18:18:26 +01:00
Paul Bottein
165a757f06 Revert entity naming in target picker chips (#27722) 2025-10-30 18:18:25 +01:00
Aidan Timson
ea8b730142 Revert "Migrate dialog-device-registry-detail to ha-wa-dialog (#27668)" (#27716)
This reverts commit 2a8d935601.
2025-10-30 18:18:24 +01:00
Paul Bottein
e88c97d625 Use entity naming in more cards (#27714)
* Use entity naming in more cards

* Migrate statistic card

* Fix localize
2025-10-30 18:18:23 +01:00
Aidan Timson
7560988b76 Trend feature: make sure content is centered when loading (#27708)
* Make sure content is centered when loading

* Restore from test
2025-10-30 18:18:22 +01:00
Aidan Timson
eecd8077b6 Calendar card height: account for title and stop overflow (#27707) 2025-10-30 18:18:21 +01:00
Paul Bottein
cbab5c3f7b Restore trigger id in overflow menu for trigger (#27702) 2025-10-30 18:18:20 +01:00
Paul Bottein
a5d27c8bb8 Only clear from and to trigger in state trigger (#27700) 2025-10-30 18:18:19 +01:00
Paul Bottein
a6a340b5db Only display add button if at least one entity is selected in entities picker (#27699) 2025-10-30 18:18:18 +01:00
173 changed files with 3152 additions and 2067 deletions

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5

View File

@@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Send bundle stats and build information to RelativeCI - name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@feb19ddc698445db27401f1490f6ac182da0816f # v3.2.0 uses: relative-ci/agent-action@c45aaa919ef85620af54242a241ac17a8fa35983 # v3.2.1
with: with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }} key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }} token: ${{ github.token }}

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
@@ -55,7 +55,7 @@ jobs:
script/release script/release
- name: Upload release assets - name: Upload release assets
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with: with:
files: | files: |
dist/*.whl dist/*.whl
@@ -75,7 +75,7 @@ jobs:
# home-assistant/wheels doesn't support SHA pinning # home-assistant/wheels doesn't support SHA pinning
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2025.10.0 uses: home-assistant/wheels@2025.11.0
with: with:
abi: cp313 abi: cp313
tag: musllinux_1_2 tag: musllinux_1_2
@@ -108,7 +108,7 @@ jobs:
- name: Tar folder - name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist . run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset - name: Upload release asset
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with: with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@@ -137,6 +137,6 @@ jobs:
- name: Tar folder - name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build . run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset - name: Upload release asset
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with: with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

View File

@@ -305,9 +305,8 @@ export class HcMain extends HassElement {
await llColl.refresh(); await llColl.refresh();
this._unsubLovelace = llColl.subscribe(async (rawConfig) => { this._unsubLovelace = llColl.subscribe(async (rawConfig) => {
if (isStrategyDashboard(rawConfig)) { if (isStrategyDashboard(rawConfig)) {
const { generateLovelaceDashboardStrategy } = await import( const { generateLovelaceDashboardStrategy } =
"../../../../src/panels/lovelace/strategies/get-strategy" await import("../../../../src/panels/lovelace/strategies/get-strategy");
);
const config = await generateLovelaceDashboardStrategy( const config = await generateLovelaceDashboardStrategy(
rawConfig, rawConfig,
this.hass! this.hass!
@@ -347,9 +346,8 @@ export class HcMain extends HassElement {
} }
private async _generateDefaultLovelaceConfig() { private async _generateDefaultLovelaceConfig() {
const { generateLovelaceDashboardStrategy } = await import( const { generateLovelaceDashboardStrategy } =
"../../../../src/panels/lovelace/strategies/get-strategy" await import("../../../../src/panels/lovelace/strategies/get-strategy");
);
this._handleNewLovelaceConfig( this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!) await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
); );

View File

@@ -18,7 +18,6 @@ import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location"; import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaHassTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant"; import { HaHassTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant";
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list"; import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import { HaNumericStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state"; import { HaNumericStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state";
import { HaPersistentNotificationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification"; import { HaPersistentNotificationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification";
import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state"; import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
@@ -38,11 +37,6 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
triggers: [{ ...HaStateTrigger.defaultConfig }], triggers: [{ ...HaStateTrigger.defaultConfig }],
}, },
{
name: "MQTT",
triggers: [{ ...HaMQTTTrigger.defaultConfig }],
},
{ {
name: "GeoLocation", name: "GeoLocation",
triggers: [{ ...HaGeolocationTrigger.defaultConfig }], triggers: [{ ...HaGeolocationTrigger.defaultConfig }],

View File

@@ -88,8 +88,8 @@ class HassioRegistriesDialog extends LitElement {
<ha-button <ha-button
?disabled=${Boolean( ?disabled=${Boolean(
!this._input.registry || !this._input.registry ||
!this._input.username || !this._input.username ||
!this._input.password !this._input.password
)} )}
@click=${this._addNewRegistry} @click=${this._addNewRegistry}
appearance="filled" appearance="filled"

View File

@@ -89,8 +89,8 @@
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1", "@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0", "@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.9.5", "@vaadin/combo-box": "24.9.6",
"@vaadin/vaadin-themable-mixin": "24.9.5", "@vaadin/vaadin-themable-mixin": "24.9.6",
"@vibrant/color": "4.0.0", "@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0", "@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10", "@webcomponents/scoped-custom-element-registry": "0.0.10",
@@ -152,13 +152,13 @@
"@babel/helper-define-polyfill-provider": "0.6.5", "@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.5", "@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5", "@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.6", "@bundle-stats/plugin-webpack-filter": "4.21.7",
"@lokalise/node-api": "15.4.0", "@lokalise/node-api": "15.4.0",
"@octokit/auth-oauth-device": "8.0.3", "@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3", "@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1", "@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.3.11", "@rsdoctor/rspack-plugin": "1.3.11",
"@rspack/core": "1.6.4", "@rspack/core": "1.6.5",
"@rspack/dev-server": "1.1.4", "@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22", "@types/chromecast-caf-receiver": "6.0.22",
@@ -178,7 +178,7 @@
"@types/tar": "6.1.13", "@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.13", "@vitest/coverage-v8": "4.0.14",
"babel-loader": "10.0.0", "babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3", "browserslist-useragent-regexp": "4.1.3",
@@ -194,7 +194,7 @@
"eslint-plugin-wc": "3.0.2", "eslint-plugin-wc": "3.0.2",
"fancy-log": "2.0.0", "fancy-log": "2.0.0",
"fs-extra": "11.3.2", "fs-extra": "11.3.2",
"glob": "12.0.0", "glob": "13.0.0",
"gulp": "5.0.1", "gulp": "5.0.1",
"gulp-brotli": "3.0.0", "gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0", "gulp-json-transform": "0.5.0",
@@ -209,7 +209,7 @@
"lodash.template": "4.5.0", "lodash.template": "4.5.0",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"pinst": "3.0.0", "pinst": "3.0.0",
"prettier": "3.6.2", "prettier": "3.7.2",
"rspack-manifest-plugin": "5.2.0", "rspack-manifest-plugin": "5.2.0",
"serve": "14.2.5", "serve": "14.2.5",
"sinon": "21.0.0", "sinon": "21.0.0",
@@ -217,9 +217,9 @@
"terser-webpack-plugin": "5.3.14", "terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.47.0", "typescript-eslint": "8.48.0",
"vite-tsconfig-paths": "5.1.4", "vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.13", "vitest": "4.0.14",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0", "webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"

View File

@@ -1,5 +1,6 @@
import type { AuthData } from "home-assistant-js-websocket"; import type { AuthData } from "home-assistant-js-websocket";
import { extractSearchParam } from "../url/search-params"; import { extractSearchParam } from "../url/search-params";
import { hassUrl } from "../../data/auth";
declare global { declare global {
interface Window { interface Window {
@@ -30,7 +31,11 @@ export function askWrite() {
export function saveTokens(tokens: AuthData | null) { export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens; tokenCache.tokens = tokens;
if (!tokenCache.writeEnabled && extractSearchParam("storeToken") === "true") { if (
!tokenCache.writeEnabled &&
(extractSearchParam("storeToken") === "true" ||
hassUrl !== `${location.protocol}//${location.host}`)
) {
tokenCache.writeEnabled = true; tokenCache.writeEnabled = true;
} }

View File

@@ -45,9 +45,8 @@ export const computeFormatFunctions = async (
formatEntityAttributeName: FormatEntityAttributeNameFunc; formatEntityAttributeName: FormatEntityAttributeNameFunc;
formatEntityName: FormatEntityNameFunc; formatEntityName: FormatEntityNameFunc;
}> => { }> => {
const { computeStateDisplay } = await import( const { computeStateDisplay } =
"../entity/compute_state_display" await import("../entity/compute_state_display");
);
const { computeAttributeValueDisplay, computeAttributeNameDisplay } = const { computeAttributeValueDisplay, computeAttributeNameDisplay } =
await import("../entity/compute_attribute_display"); await import("../entity/compute_attribute_display");

View File

@@ -593,6 +593,7 @@ export class HaChartBase extends LitElement {
} }
const options = { const options = {
animation: !this._reducedMotion, animation: !this._reducedMotion,
animationDuration: 500,
darkMode: this._themes.darkMode ?? false, darkMode: this._themes.darkMode ?? false,
aria: { show: true }, aria: { show: true },
dataZoom: this._getDataZoomConfig(), dataZoom: this._getDataZoomConfig(),

View File

@@ -167,6 +167,7 @@ export class HaSankeyChart extends LitElement {
curveness: 0.5, curveness: 0.5,
}, },
layoutIterations: 0, layoutIterations: 0,
animationDuration: 500,
label: { label: {
formatter: (params) => formatter: (params) =>
data.nodes.find((node) => node.id === (params.data as Node).id) data.nodes.find((node) => node.id === (params.data as Node).id)
@@ -279,6 +280,7 @@ export class HaSankeyChart extends LitElement {
:host { :host {
display: block; display: block;
flex: 1; flex: 1;
max-width: 100%;
background: var(--ha-card-background, var(--card-background-color)); background: var(--ha-card-background, var(--card-background-color));
} }
ha-chart-base { ha-chart-base {

View File

@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import type { VisualMapComponentOption } from "echarts/components"; import type { VisualMapComponentOption } from "echarts/components";
import type { LineSeriesOption } from "echarts/charts"; import type { LineSeriesOption } from "echarts/charts";
import type { YAXisOption } from "echarts/types/dist/shared"; import type { YAXisOption } from "echarts/types/dist/shared";
@@ -27,6 +27,7 @@ const safeParseFloat = (value) => {
return isFinite(parsed) ? parsed : null; return isFinite(parsed) ? parsed : null;
}; };
@customElement("state-history-chart-line")
export class StateHistoryChartLine extends LitElement { export class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -795,7 +796,6 @@ export class StateHistoryChartLine extends LitElement {
return Math.abs(value) < 1 ? value : roundingFn(value); return Math.abs(value) < 1 ? value : roundingFn(value);
} }
} }
customElements.define("state-history-chart-line", StateHistoryChartLine);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -838,10 +838,10 @@ export class HaDataTable extends LitElement {
} else if (this.sortDirection === "asc") { } else if (this.sortDirection === "asc") {
this.sortDirection = "desc"; this.sortDirection = "desc";
} else { } else {
this.sortDirection = null; this.sortDirection = "asc";
} }
this.sortColumn = this.sortDirection === null ? undefined : columnId; this.sortColumn = columnId;
fireEvent(this, "sorting-changed", { fireEvent(this, "sorting-changed", {
column: columnId, column: columnId,

View File

@@ -2,7 +2,7 @@ import { mdiAlert } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit"; import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
@@ -17,6 +17,7 @@ import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-state-icon"; import "../ha-state-icon";
@customElement("state-badge")
export class StateBadge extends LitElement { export class StateBadge extends LitElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;
@@ -265,5 +266,3 @@ declare global {
"state-badge": StateBadge; "state-badge": StateBadge;
} }
} }
customElements.define("state-badge", StateBadge);

View File

@@ -659,6 +659,7 @@ export class HaAssistChat extends LitElement {
--markdown-table-border-color: var(--divider-color); --markdown-table-border-color: var(--divider-color);
--markdown-code-background-color: var(--primary-background-color); --markdown-code-background-color: var(--primary-background-color);
--markdown-code-text-color: var(--primary-text-color); --markdown-code-text-color: var(--primary-text-color);
--markdown-list-indent: 1rem;
&:not(:has(ha-markdown-element)) { &:not(:has(ha-markdown-element)) {
min-height: 1lh; min-height: 1lh;
min-width: 1lh; min-width: 1lh;

View File

@@ -21,7 +21,8 @@ export class HaBottomSheet extends LitElement {
private _isDragging = false; private _isDragging = false;
private _handleAfterHide() { private _handleAfterHide(afterHideEvent: Event) {
afterHideEvent.stopPropagation();
this.open = false; this.open = false;
const ev = new Event("closed", { const ev = new Event("closed", {
bubbles: true, bubbles: true,

View File

@@ -202,6 +202,7 @@ export class HaControlSelect extends LitElement {
color: var(--primary-text-color); color: var(--primary-text-color);
user-select: none; user-select: none;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
border-radius: var(--control-select-border-radius);
} }
:host([vertical]) { :host([vertical]) {
width: var(--control-select-thickness); width: var(--control-select-thickness);
@@ -211,7 +212,6 @@ export class HaControlSelect extends LitElement {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
border-radius: var(--control-select-border-radius);
transform: translateZ(0); transform: translateZ(0);
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@@ -248,7 +248,7 @@ export class HaGenericPicker extends LitElement {
}); });
}; };
private _hidePicker(ev) { private _hidePicker(ev: Event) {
ev.stopPropagation(); ev.stopPropagation();
if (this._newValue) { if (this._newValue) {
fireEvent(this, "value-changed", { value: this._newValue }); fireEvent(this, "value-changed", { value: this._newValue });

View File

@@ -73,6 +73,8 @@ export class HaLanguagePicker extends LitElement {
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@property() public helper?: string;
@property({ attribute: "native-name", type: Boolean }) @property({ attribute: "native-name", type: Boolean })
public nativeName = false; public nativeName = false;
@@ -135,6 +137,7 @@ export class HaLanguagePicker extends LitElement {
.value=${value} .value=${value}
.valueRenderer=${this._valueRenderer} .valueRenderer=${this._valueRenderer}
.disabled=${this.disabled} .disabled=${this.disabled}
.helper=${this.helper}
.getItems=${this._getItems} .getItems=${this._getItems}
@value-changed=${this._changed} @value-changed=${this._changed}
hide-clear-icon hide-clear-icon

View File

@@ -71,7 +71,7 @@ class HaMarkdownElement extends ReactiveElement {
if (!this.innerHTML && this.cache) { if (!this.innerHTML && this.cache) {
const key = this._computeCacheKey(); const key = this._computeCacheKey();
if (markdownCache.has(key)) { if (markdownCache.has(key)) {
render(markdownCache.get(key)!, this.renderRoot); render(h(unsafeHTML(markdownCache.get(key))), this.renderRoot);
this._resize(); this._resize();
} }
} }

View File

@@ -71,13 +71,11 @@ export class HaMarkdown extends LitElement {
color: var(--markdown-link-color, var(--primary-color)); color: var(--markdown-link-color, var(--primary-color));
} }
img { img {
background-color: rgba(10, 10, 10, 0.15); background-color: var(--markdown-image-background-color);
border-radius: var(--markdown-image-border-radius); border-radius: var(--markdown-image-border-radius);
max-width: 100%; max-width: 100%;
min-height: 2lh;
height: auto; height: auto;
width: auto; width: auto;
text-indent: 4px;
transition: height 0.2s ease-in-out; transition: height 0.2s ease-in-out;
} }
p:first-child > img:first-child { p:first-child > img:first-child {
@@ -86,10 +84,9 @@ export class HaMarkdown extends LitElement {
p:first-child > img:last-child { p:first-child > img:last-child {
vertical-align: top; vertical-align: top;
} }
ol, :host > ul,
ul { :host > ol {
list-style-position: inside; padding-inline-start: var(--markdown-list-indent, revert);
padding-inline-start: 0;
} }
li { li {
&:has(input[type="checkbox"]) { &:has(input[type="checkbox"]) {
@@ -140,16 +137,19 @@ export class HaMarkdown extends LitElement {
margin: var(--ha-space-4) 0; margin: var(--ha-space-4) 0;
} }
table { table {
border-collapse: collapse; border-collapse: var(--markdown-table-border-collapse, collapse);
display: block; }
overflow-x: auto; div:has(> table) {
overflow: auto;
} }
th { th {
text-align: start; text-align: start;
} }
td, td,
th { th {
border: 1px solid var(--markdown-table-border-color, transparent); border-width: var(--markdown-table-border-width, 1px);
border-style: var(--markdown-table-border-style, solid);
border-color: var(--markdown-table-border-color, var(--divider-color));
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
} }
blockquote { blockquote {

View File

@@ -103,8 +103,8 @@ export class HaPickerField extends LitElement {
--md-list-item-two-line-container-height: 56px; --md-list-item-two-line-container-height: 56px;
--md-list-item-top-space: 0px; --md-list-item-top-space: 0px;
--md-list-item-bottom-space: 0px; --md-list-item-bottom-space: 0px;
--md-list-item-leading-space: 8px; --md-list-item-leading-space: var(--ha-space-4);
--md-list-item-trailing-space: 8px; --md-list-item-trailing-space: var(--ha-space-2);
--ha-md-list-item-gap: var(--ha-space-2); --ha-md-list-item-gap: var(--ha-space-2);
/* Remove the default focus ring */ /* Remove the default focus ring */
--md-focus-ring-width: 0px; --md-focus-ring-width: 0px;

View File

@@ -450,7 +450,7 @@ export class HaServiceControl extends LitElement {
const hasOptional = Boolean( const hasOptional = Boolean(
!shouldRenderServiceDataYaml && !shouldRenderServiceDataYaml &&
serviceData?.flatFields.some((field) => showOptionalToggle(field)) serviceData?.flatFields.some((field) => showOptionalToggle(field))
); );
const targetEntities = this._getTargetedEntities( const targetEntities = this._getTargetedEntities(
@@ -467,7 +467,7 @@ export class HaServiceControl extends LitElement {
const descriptionPlaceholders = const descriptionPlaceholders =
domain && serviceName domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders ? this.hass.services[domain]?.[serviceName]?.description_placeholders
: undefined; : undefined;
const description = const description =

View File

@@ -197,6 +197,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _mouseLeaveTimeout?: number; private _mouseLeaveTimeout?: number;
private _touchendTimeout?: number;
private _tooltipHideTimeout?: number; private _tooltipHideTimeout?: number;
private _recentKeydownActiveUntil = 0; private _recentKeydownActiveUntil = 0;
@@ -237,6 +239,18 @@ class HaSidebar extends SubscribeMixin(LitElement) {
]; ];
} }
public disconnectedCallback() {
super.disconnectedCallback();
// clear timeouts
clearTimeout(this._mouseLeaveTimeout);
clearTimeout(this._tooltipHideTimeout);
clearTimeout(this._touchendTimeout);
// set undefined values
this._mouseLeaveTimeout = undefined;
this._tooltipHideTimeout = undefined;
this._touchendTimeout = undefined;
}
protected render() { protected render() {
if (!this.hass) { if (!this.hass) {
return nothing; return nothing;
@@ -406,6 +420,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
class="ha-scrollbar" class="ha-scrollbar"
@focusin=${this._listboxFocusIn} @focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut} @focusout=${this._listboxFocusOut}
@touchend=${this._listboxTouchend}
@scroll=${this._listboxScroll} @scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown} @keydown=${this._listboxKeydown}
> >
@@ -620,6 +635,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._hideTooltip(); this._hideTooltip();
} }
private _listboxTouchend() {
clearTimeout(this._touchendTimeout);
this._touchendTimeout = window.setTimeout(() => {
// Allow 1 second for users to read the tooltip on touch devices
this._hideTooltip();
}, 1000);
}
@eventOptions({ @eventOptions({
passive: true, passive: true,
}) })

View File

@@ -6,7 +6,6 @@ import {
mdiDevices, mdiDevices,
mdiFormatListBulleted, mdiFormatListBulleted,
mdiGestureDoubleTap, mdiGestureDoubleTap,
mdiHomeAssistant,
mdiMapMarker, mdiMapMarker,
mdiMapMarkerRadius, mdiMapMarkerRadius,
mdiMessageAlert, mdiMessageAlert,
@@ -23,6 +22,7 @@ import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until"; import { until } from "lit/directives/until";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons"; import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-icon"; import "./ha-icon";
import "./ha-svg-icon"; import "./ha-svg-icon";

View File

@@ -1,3 +1,5 @@
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { import {
customElement, customElement,
@@ -7,8 +9,6 @@ import {
state, state,
} from "lit/decorators"; } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { mdiClose } from "@mdi/js";
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@@ -53,6 +53,7 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface. * @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
* *
* @attr {boolean} open - Controls the dialog open state. * @attr {boolean} open - Controls the dialog open state.
* @attr {("alert"|"standard")} type - Dialog type. Defaults to "standard".
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium". * @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false. * @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used. * @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
@@ -84,6 +85,9 @@ export class HaWaDialog extends LitElement {
@property({ type: Boolean, reflect: true }) @property({ type: Boolean, reflect: true })
public open = false; public open = false;
@property({ reflect: true })
public type: "alert" | "standard" = "standard";
@property({ type: String, reflect: true, attribute: "width" }) @property({ type: String, reflect: true, attribute: "width" })
public width: DialogWidth = "medium"; public width: DialogWidth = "medium";
@@ -172,7 +176,9 @@ export class HaWaDialog extends LitElement {
await this.updateComplete; await this.updateComplete;
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus(); requestAnimationFrame(() => {
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
});
}; };
private _handleAfterShow = () => { private _handleAfterShow = () => {
@@ -198,18 +204,7 @@ export class HaWaDialog extends LitElement {
haStyleScrollbar, haStyleScrollbar,
css` css`
wa-dialog { wa-dialog {
--full-width: var( --full-width: var(--ha-dialog-width-full, min(95vw, var(--safe-width)));
--ha-dialog-width-full,
min(
95vw,
calc(
100vw - var(--safe-area-inset-left, var(--ha-space-0)) - var(
--safe-area-inset-right,
var(--ha-space-0)
)
)
)
);
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width)); --width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
--spacing: var(--dialog-content-padding, var(--ha-space-6)); --spacing: var(--dialog-content-padding, var(--ha-space-6));
--show-duration: var(--ha-dialog-show-duration, 200ms); --show-duration: var(--ha-dialog-show-duration, 200ms);
@@ -226,8 +221,7 @@ export class HaWaDialog extends LitElement {
--ha-dialog-border-radius, --ha-dialog-border-radius,
var(--ha-border-radius-3xl) var(--ha-border-radius-3xl)
); );
max-width: var(--ha-dialog-max-width, 100vw); max-width: var(--ha-dialog-max-width, var(--safe-width));
max-width: var(--ha-dialog-max-width, 100svw);
} }
:host([width="small"]) wa-dialog { :host([width="small"]) wa-dialog {
@@ -247,34 +241,57 @@ export class HaWaDialog extends LitElement {
max-width: var(--width, var(--full-width)); max-width: var(--width, var(--full-width));
max-height: var( max-height: var(
--ha-dialog-max-height, --ha-dialog-max-height,
calc(100% - var(--ha-space-20)) calc(var(--safe-height) - var(--ha-space-20))
); );
min-height: var(--ha-dialog-min-height); min-height: var(--ha-dialog-min-height);
position: var(--dialog-surface-position, relative); position: var(--dialog-surface-position, relative);
margin-top: var(--dialog-surface-margin-top, auto); margin-top: var(--dialog-surface-margin-top, auto);
/* Used to offset the dialog from the safe areas when space is limited */
transform: translate(
calc(
var(--safe-area-offset-left, var(--ha-space-0)) - var(
--safe-area-offset-right,
var(--ha-space-0)
)
),
calc(
var(--safe-area-offset-top, var(--ha-space-0)) - var(
--safe-area-offset-bottom,
var(--ha-space-0)
)
)
);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
:host { :host([type="standard"]) {
--ha-dialog-border-radius: var(--ha-space-0); --ha-dialog-border-radius: var(--ha-space-0);
}
wa-dialog { wa-dialog {
--full-width: var(--ha-dialog-width-full, 100vw); /* Make the container fill the whole screen width and not the safe width */
} --full-width: var(--ha-dialog-width-full, 100vw);
--width: var(--full-width);
}
wa-dialog::part(dialog) { wa-dialog::part(dialog) {
min-height: var(--ha-dialog-min-height, 100vh); /* Make the dialog fill the whole screen height and not the safe height */
min-height: var(--ha-dialog-min-height, 100svh); min-height: var(--ha-dialog-min-height, 100vh);
max-height: var(--ha-dialog-max-height, 100vh); min-height: var(--ha-dialog-min-height, 100dvh);
max-height: var(--ha-dialog-max-height, 100svh); max-height: var(--ha-dialog-max-height, 100vh);
padding-top: var(--safe-area-inset-top, var(--ha-space-0)); max-height: var(--ha-dialog-max-height, 100dvh);
padding-bottom: var(--safe-area-inset-bottom, var(--ha-space-0)); margin-top: 0;
padding-left: var(--safe-area-inset-left, var(--ha-space-0)); margin-bottom: 0;
padding-right: var(--safe-area-inset-right, var(--ha-space-0)); /* Use safe area as padding instead of the container size */
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
/* Reset the transform to center the dialog */
transform: none;
}
} }
} }

View File

@@ -1,10 +1,11 @@
import { LitElement, html, css } from "lit"; import { LitElement, html, css } from "lit";
import { property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../ha-state-icon"; import "../ha-state-icon";
@customElement("ha-entity-marker")
class HaEntityMarker extends LitElement { class HaEntityMarker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -89,8 +90,6 @@ class HaEntityMarker extends LitElement {
`; `;
} }
customElements.define("ha-entity-marker", HaEntityMarker);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-entity-marker": HaEntityMarker; "ha-entity-marker": HaEntityMarker;

View File

@@ -75,17 +75,11 @@ export const reorderAreaRegistryEntries = (
}); });
export const getAreaEntityLookup = ( export const getAreaEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[], entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
filterHidden = false
): AreaEntityLookup => { ): AreaEntityLookup => {
const areaEntityLookup: AreaEntityLookup = {}; const areaEntityLookup: AreaEntityLookup = {};
for (const entity of entities) { for (const entity of entities) {
if ( if (!entity.area_id) {
!entity.area_id ||
(filterHidden &&
((entity as EntityRegistryDisplayEntry).hidden ||
(entity as EntityRegistryEntry).hidden_by))
) {
continue; continue;
} }
if (!(entity.area_id in areaEntityLookup)) { if (!(entity.area_id in areaEntityLookup)) {

View File

@@ -114,12 +114,6 @@ export interface StateTrigger extends BaseTrigger {
for?: string | number | ForDict; for?: string | number | ForDict;
} }
export interface MqttTrigger extends BaseTrigger {
trigger: "mqtt";
topic: string;
payload?: string;
}
export interface GeoLocationTrigger extends BaseTrigger { export interface GeoLocationTrigger extends BaseTrigger {
trigger: "geo_location"; trigger: "geo_location";
source: string; source: string;
@@ -127,6 +121,12 @@ export interface GeoLocationTrigger extends BaseTrigger {
event: "enter" | "leave"; event: "enter" | "leave";
} }
export interface MqttTrigger extends BaseTrigger {
trigger: "mqtt";
topic: string;
payload?: string;
}
export interface HassTrigger extends BaseTrigger { export interface HassTrigger extends BaseTrigger {
trigger: "homeassistant"; trigger: "homeassistant";
event: "start" | "shutdown"; event: "start" | "shutdown";

View File

@@ -144,9 +144,7 @@ const tryDescribeTrigger = (
const type = getTriggerObjectId(trigger.trigger); const type = getTriggerObjectId(trigger.trigger);
return ( return (
hass.localize( hass.localize(`component.${domain}.triggers.${type}.name`) ||
`component.${domain}.triggers.${type}.description_configured`
) ||
hass.localize( hass.localize(
`ui.panel.config.automation.editor.triggers.type.${triggerType as LegacyTrigger["trigger"]}.label` `ui.panel.config.automation.editor.triggers.type.${triggerType as LegacyTrigger["trigger"]}.label`
) || ) ||
@@ -919,9 +917,7 @@ const tryDescribeCondition = (
const type = getConditionObjectId(condition.condition); const type = getConditionObjectId(condition.condition);
return ( return (
hass.localize( hass.localize(`component.${domain}.conditions.${type}.name`) ||
`component.${domain}.conditions.${type}.description_configured`
) ||
hass.localize( hass.localize(
`ui.panel.config.automation.editor.conditions.type.${conditionType as LegacyCondition["condition"]}.label` `ui.panel.config.automation.editor.conditions.type.${conditionType as LegacyCondition["condition"]}.label`
) || ) ||

View File

@@ -111,17 +111,11 @@ export const sortDeviceRegistryByName = (
); );
export const getDeviceEntityLookup = ( export const getDeviceEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[], entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
filterHidden = false
): DeviceEntityLookup => { ): DeviceEntityLookup => {
const deviceEntityLookup: DeviceEntityLookup = {}; const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) { for (const entity of entities) {
if ( if (!entity.device_id) {
!entity.device_id ||
(filterHidden &&
((entity as EntityRegistryDisplayEntry).hidden ||
(entity as EntityRegistryEntry).hidden_by))
) {
continue; continue;
} }
if (!(entity.device_id in deviceEntityLookup)) { if (!(entity.device_id in deviceEntityLookup)) {

14
src/data/esphome.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { HomeAssistant } from "../types";
export interface ESPHomeEncryptionKey {
encryption_key: string;
}
export const fetchESPHomeEncryptionKey = (
hass: HomeAssistant,
entry_id: string
): Promise<ESPHomeEncryptionKey> =>
hass.callWS({
type: "esphome/get_encryption_key",
entry_id,
});

View File

@@ -1,4 +1,3 @@
import { stringCompare } from "../common/string/compare";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area_registry"; import type { AreaRegistryEntry } from "./area_registry";
import type { RegistryEntry } from "./registry"; import type { RegistryEntry } from "./registry";
@@ -75,27 +74,3 @@ export const getFloorAreaLookup = (
} }
return floorAreaLookup; return floorAreaLookup;
}; };
export const floorCompare =
(entries?: HomeAssistant["floors"], order?: string[]) =>
(a: string, b: string) => {
const indexA = order ? order.indexOf(a) : -1;
const indexB = order ? order.indexOf(b) : -1;
if (indexA === -1 && indexB === -1) {
const floorA = entries?.[a];
const floorB = entries?.[b];
if (floorA && floorB && floorA.level !== floorB.level) {
return (floorB.level ?? -9999) - (floorA.level ?? -9999);
}
const nameA = floorA?.name ?? a;
const nameB = floorB?.name ?? b;
return stringCompare(nameA, nameB);
}
if (indexA === -1) {
return 1;
}
if (indexB === -1) {
return -1;
}
return indexA - indexB;
};

View File

@@ -47,8 +47,7 @@ export interface HassioFullBackupCreateParams {
confirm_password?: string; confirm_password?: string;
background?: boolean; background?: boolean;
} }
export interface HassioPartialBackupCreateParams export interface HassioPartialBackupCreateParams extends HassioFullBackupCreateParams {
extends HassioFullBackupCreateParams {
folders?: string[]; folders?: string[];
addons?: string[]; addons?: string[];
homeassistant?: boolean; homeassistant?: boolean;

View File

@@ -18,8 +18,7 @@ export const enum LawnMowerEntityFeature {
} }
interface LawnMowerEntityAttributes interface LawnMowerEntityAttributes
extends HassEntityAttributeBase, extends HassEntityAttributeBase, Record<string, any> {}
Record<string, any> {}
export interface LawnMowerEntity extends HassEntityBase { export interface LawnMowerEntity extends HassEntityBase {
attributes: LawnMowerEntityAttributes; attributes: LawnMowerEntityAttributes;

View File

@@ -18,8 +18,7 @@ export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
cards?: LovelaceCardConfig[]; cards?: LovelaceCardConfig[];
} }
export interface LovelaceStrategySectionConfig export interface LovelaceStrategySectionConfig extends LovelaceBaseSectionConfig {
extends LovelaceBaseSectionConfig {
strategy: LovelaceStrategyConfig; strategy: LovelaceStrategyConfig;
} }

View File

@@ -11,8 +11,7 @@ export interface LovelaceConfig extends LovelaceDashboardBaseConfig {
views: LovelaceViewRawConfig[]; views: LovelaceViewRawConfig[];
} }
export interface LovelaceDashboardStrategyConfig export interface LovelaceDashboardStrategyConfig extends LovelaceDashboardBaseConfig {
extends LovelaceDashboardBaseConfig {
strategy: LovelaceStrategyConfig; strategy: LovelaceStrategyConfig;
} }

View File

@@ -29,8 +29,7 @@ export interface LovelaceDashboardMutableParams {
title: string; title: string;
} }
export interface LovelaceDashboardCreateParams export interface LovelaceDashboardCreateParams extends LovelaceDashboardMutableParams {
extends LovelaceDashboardMutableParams {
url_path: string; url_path: string;
mode: "storage"; mode: "storage";
} }

View File

@@ -106,8 +106,7 @@ export interface AutomationTrace extends BaseTrace {
} }
export interface AutomationTraceExtended export interface AutomationTraceExtended
extends AutomationTrace, extends AutomationTrace, BaseTraceExtended {
BaseTraceExtended {
config: ManualAutomationConfig; config: ManualAutomationConfig;
blueprint_inputs?: BlueprintAutomationConfig; blueprint_inputs?: BlueprintAutomationConfig;
} }

View File

@@ -40,7 +40,6 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
event: {}, event: {},
geo_location: {}, geo_location: {},
homeassistant: {}, homeassistant: {},
mqtt: {},
conversation: {}, conversation: {},
tag: {}, tag: {},
template: {}, template: {},

View File

@@ -1,6 +1,7 @@
import { mdiAlertOutline, mdiClose } from "@mdi/js"; import { mdiAlertOutline, mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button"; import "../../components/ha-button";
@@ -64,6 +65,7 @@ class DialogBox extends LitElement {
<ha-wa-dialog <ha-wa-dialog
.hass=${this.hass} .hass=${this.hass}
.open=${this._open} .open=${this._open}
type=${confirmPrompt ? "alert" : "standard"}
?prevent-scrim-close=${confirmPrompt} ?prevent-scrim-close=${confirmPrompt}
@closed=${this._dialogClosed} @closed=${this._dialogClosed}
aria-labelledby="dialog-box-title" aria-labelledby="dialog-box-title"
@@ -79,7 +81,11 @@ class DialogBox extends LitElement {
></ha-icon-button ></ha-icon-button
></slot>` ></slot>`
: nothing} : nothing}
<span slot="title" id="dialog-box-title"> <span
class=${classMap({ title: true, alert: confirmPrompt })}
slot="title"
id="dialog-box-title"
>
${this._params.warning ${this._params.warning
? html`<ha-svg-icon ? html`<ha-svg-icon
.path=${mdiAlertOutline} .path=${mdiAlertOutline}
@@ -199,6 +205,14 @@ class DialogBox extends LitElement {
ha-textfield { ha-textfield {
width: 100%; width: 100%;
} }
.title.alert {
padding: 0 var(--ha-space-2);
}
@media all and (min-width: 450px) and (min-height: 500px) {
.title.alert {
padding: 0 var(--ha-space-1);
}
}
`; `;
} }

View File

@@ -32,8 +32,7 @@ export interface PromptDialogParams extends BaseDialogBoxParams {
} }
export interface DialogBoxParams export interface DialogBoxParams
extends ConfirmationDialogParams, extends ConfirmationDialogParams, PromptDialogParams {
PromptDialogParams {
confirm?: (out?: string) => void; confirm?: (out?: string) => void;
confirmation?: boolean; confirmation?: boolean;
prompt?: boolean; prompt?: boolean;

View File

@@ -1,9 +1,9 @@
import type { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
import { ancestorsWithProperty } from "../common/dom/ancestors-with-property"; import { ancestorsWithProperty } from "../common/dom/ancestors-with-property";
import { deepActiveElement } from "../common/dom/deep-active-element"; import { deepActiveElement } from "../common/dom/deep-active-element";
import type { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
import { nextRender } from "../common/util/render-status"; import { nextRender } from "../common/util/render-status";
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
declare global { declare global {
// for fire event // for fire event
@@ -19,10 +19,11 @@ declare global {
} }
} }
export interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]> export interface HassDialog<
extends HTMLElement { T = HASSDomEvents[ValidHassDomEvent],
> extends HTMLElement {
showDialog(params: T); showDialog(params: T);
closeDialog?: () => boolean; closeDialog?: (historyState?: any) => boolean;
} }
interface ShowDialogParams<T> { interface ShowDialogParams<T> {
@@ -143,27 +144,32 @@ export const showDialog = async (
return true; return true;
}; };
export const closeDialog = async (dialogTag: string): Promise<boolean> => { export const closeDialog = async (
dialogTag: string,
historyState?: any
): Promise<boolean> => {
if (!(dialogTag in LOADED)) { if (!(dialogTag in LOADED)) {
return true; return true;
} }
const dialogElement = await LOADED[dialogTag].element; const dialogElement = await LOADED[dialogTag].element;
if (dialogElement.closeDialog) { if (dialogElement.closeDialog) {
return dialogElement.closeDialog() !== false; return dialogElement.closeDialog(historyState) !== false;
} }
return true; return true;
}; };
// called on back() // called on back()
export const closeLastDialog = async () => { export const closeLastDialog = async (historyState?: any) => {
if (OPEN_DIALOG_STACK.length) { if (OPEN_DIALOG_STACK.length) {
const lastDialog = OPEN_DIALOG_STACK.pop(); const lastDialog = OPEN_DIALOG_STACK.pop() as DialogState;
const closed = await closeDialog(lastDialog!.dialogTag); const closed = await closeDialog(lastDialog.dialogTag, historyState);
if (!closed) { if (!closed) {
// if the dialog was not closed, put it back on the stack // if the dialog was not closed, put it back on the stack
OPEN_DIALOG_STACK.push(lastDialog!); OPEN_DIALOG_STACK.push(lastDialog);
} } else if (
if (OPEN_DIALOG_STACK.length && mainWindow.history.state?.opensDialog) { OPEN_DIALOG_STACK.length &&
mainWindow.history.state?.opensDialog
) {
// if there are more dialogs open, push a new state so back() will close the next top dialog // if there are more dialogs open, push a new state so back() will close the next top dialog
mainWindow.history.pushState( mainWindow.history.pushState(
{ dialog: OPEN_DIALOG_STACK[OPEN_DIALOG_STACK.length - 1].dialogTag }, { dialog: OPEN_DIALOG_STACK[OPEN_DIALOG_STACK.length - 1].dialogTag },

View File

@@ -1,5 +1,5 @@
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { slugify } from "../../../common/string/slugify"; import { slugify } from "../../../common/string/slugify";
import "../../../components/buttons/ha-progress-button"; import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-camera-stream"; import "../../../components/ha-camera-stream";
@@ -9,6 +9,7 @@ import type { HomeAssistant } from "../../../types";
import { fileDownload } from "../../../util/file_download"; import { fileDownload } from "../../../util/file_download";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
@customElement("more-info-camera")
class MoreInfoCamera extends LitElement { class MoreInfoCamera extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -112,8 +113,6 @@ class MoreInfoCamera extends LitElement {
`; `;
} }
customElements.define("more-info-camera", MoreInfoCamera);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"more-info-camera": MoreInfoCamera; "more-info-camera": MoreInfoCamera;

View File

@@ -7,7 +7,7 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit"; import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon"; import "../../../components/ha-attribute-icon";
@@ -32,6 +32,7 @@ import { moreInfoControlStyle } from "../components/more-info-control-style";
type MainControl = "temperature" | "humidity"; type MainControl = "temperature" | "humidity";
@customElement("more-info-climate")
class MoreInfoClimate extends LitElement { class MoreInfoClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -567,8 +568,6 @@ class MoreInfoClimate extends LitElement {
} }
} }
customElements.define("more-info-climate", MoreInfoClimate);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"more-info-climate": MoreInfoClimate; "more-info-climate": MoreInfoClimate;

View File

@@ -1,7 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit"; import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import type { GroupEntity } from "../../../data/group"; import type { GroupEntity } from "../../../data/group";
import { computeGroupDomain } from "../../../data/group"; import { computeGroupDomain } from "../../../data/group";
@@ -13,6 +13,7 @@ import {
importMoreInfoControl, importMoreInfoControl,
} from "../state_more_info_control"; } from "../state_more_info_control";
@customElement("more-info-group")
class MoreInfoGroup extends LitElement { class MoreInfoGroup extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -106,8 +107,6 @@ class MoreInfoGroup extends LitElement {
} }
} }
customElements.define("more-info-group", MoreInfoGroup);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"more-info-group": MoreInfoGroup; "more-info-group": MoreInfoGroup;

View File

@@ -1,7 +1,7 @@
import { mdiPower, mdiTuneVariant } from "@mdi/js"; import { mdiPower, mdiTuneVariant } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit"; import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-select-menu"; import "../../../components/ha-control-select-menu";
@@ -15,6 +15,7 @@ import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container"; import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style"; import { moreInfoControlStyle } from "../components/more-info-control-style";
@customElement("more-info-humidifier")
class MoreInfoHumidifier extends LitElement { class MoreInfoHumidifier extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -249,8 +250,6 @@ class MoreInfoHumidifier extends LitElement {
} }
} }
customElements.define("more-info-humidifier", MoreInfoHumidifier);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"more-info-humidifier": MoreInfoHumidifier; "more-info-humidifier": MoreInfoHumidifier;

View File

@@ -63,11 +63,6 @@ const _SHORTCUTS: Section[] = [
descriptionTranslationKey: descriptionTranslationKey:
"ui.dialogs.shortcuts.searching.search_in_table", "ui.dialogs.shortcuts.searching.search_in_table",
}, },
{
shortcut: ["N"],
descriptionTranslationKey:
"ui.dialogs.shortcuts.searching.new_in_table",
},
], ],
}, },
{ {

View File

@@ -1,9 +1,10 @@
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../components/ha-spinner"; import "../components/ha-spinner";
import "../components/ha-button"; import "../components/ha-button";
@customElement("ha-init-page")
class HaInitPage extends LitElement { class HaInitPage extends LitElement {
@property({ type: Boolean }) public error = false; @property({ type: Boolean }) public error = false;
@@ -120,8 +121,6 @@ class HaInitPage extends LitElement {
`; `;
} }
customElements.define("ha-init-page", HaInitPage);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-init-page": HaInitPage; "ha-init-page": HaInitPage;

View File

@@ -128,6 +128,8 @@ class HassSubpage extends LitElement {
ha-menu-button, ha-menu-button,
ha-icon-button-arrow-prev, ha-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) { ::slotted([slot="toolbar-icon"]) {
display: flex;
align-items: center;
pointer-events: auto; pointer-events: auto;
color: var(--sidebar-icon-color); color: var(--sidebar-icon-color);
} }
@@ -143,7 +145,6 @@ class HassSubpage extends LitElement {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding-bottom: 1px;
} }
.content { .content {

View File

@@ -190,21 +190,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
}; };
} }
protected supportedSingleKeyShortcuts(): SupportedShortcuts {
if (this.hasFab) {
return {
n: () => {
const fab = this.querySelector<HTMLElement>('[slot="fab"]');
if (fab) {
fab.click();
}
},
};
}
return {};
}
private _showPaneController = new ResizeController(this, { private _showPaneController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width > 750, callback: (entries) => entries[0]?.contentRect.width > 750,
}); });
@@ -636,9 +621,9 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
} else if (this._sortDirection === "asc") { } else if (this._sortDirection === "asc") {
this._sortDirection = "desc"; this._sortDirection = "desc";
} else { } else {
this._sortDirection = null; this._sortDirection = "asc";
} }
this._sortColumn = this._sortDirection === null ? undefined : columnId; this._sortColumn = columnId;
fireEvent(this, "sorting-changed", { fireEvent(this, "sorting-changed", {
column: columnId, column: columnId,

View File

@@ -1,6 +1,6 @@
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import type { LocalizeKeys } from "../common/translations/localize"; import type { LocalizeKeys } from "../common/translations/localize";
import "../components/ha-button"; import "../components/ha-button";
import "../components/ha-icon-button"; import "../components/ha-icon-button";
@@ -26,6 +26,7 @@ export interface ToastActionParams {
| { translationKey: LocalizeKeys; args?: Record<string, string> }; | { translationKey: LocalizeKeys; args?: Record<string, string> };
} }
@customElement("notification-manager")
class NotificationManager extends LitElement { class NotificationManager extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -115,8 +116,6 @@ class NotificationManager extends LitElement {
} }
} }
customElements.define("notification-manager", NotificationManager);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"notification-manager": NotificationManager; "notification-manager": NotificationManager;

View File

@@ -90,9 +90,7 @@ class OnboardingRestoreBackupCloudLogin extends LitElement {
this._email = this._cloudLoginElement.emailField.value; this._email = this._cloudLoginElement.emailField.value;
} }
await import( await import("../../panels/config/cloud/forgot-password/cloud-forgot-password-card");
"../../panels/config/cloud/forgot-password/cloud-forgot-password-card"
);
this._view = "forgot-password"; this._view = "forgot-password";
} }

View File

@@ -3,7 +3,7 @@ import { TZDate } from "@date-fns/tz";
import { addDays, isSameDay } from "date-fns"; import { addDays, isSameDay } from "date-fns";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { formatDate } from "../../common/datetime/format_date"; import { formatDate } from "../../common/datetime/format_date";
import { formatDateTime } from "../../common/datetime/format_date_time"; import { formatDateTime } from "../../common/datetime/format_date_time";
import { formatTime } from "../../common/datetime/format_time"; import { formatTime } from "../../common/datetime/format_time";
@@ -26,6 +26,7 @@ import type { CalendarEventDetailDialogParams } from "./show-dialog-calendar-eve
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor"; import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone"; import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
@customElement("dialog-calendar-event-detail")
class DialogCalendarEventDetail extends LitElement { class DialogCalendarEventDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -271,8 +272,3 @@ declare global {
"dialog-calendar-event-detail": DialogCalendarEventDetail; "dialog-calendar-event-detail": DialogCalendarEventDetail;
} }
} }
customElements.define(
"dialog-calendar-event-detail",
DialogCalendarEventDetail
);

View File

@@ -13,6 +13,7 @@ import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strateg
import type { Lovelace } from "../lovelace/types"; import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view"; import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container"; import "../lovelace/views/hui-view-container";
import "../lovelace/views/hui-view-background";
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = { const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: { strategy: {
@@ -115,6 +116,7 @@ class PanelClimate extends LitElement {
this._lovelace this._lovelace
? html` ? html`
<hui-view-container .hass=${this.hass}> <hui-view-container .hass=${this.hass}>
<hui-view-background .hass=${this.hass}> </hui-view-background>
<hui-view <hui-view
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}

View File

@@ -1,7 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-picker"; import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker"; import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
@@ -40,6 +40,7 @@ const SENSOR_DOMAINS = ["sensor"];
const TEMPERATURE_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_TEMPERATURE]; const TEMPERATURE_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_TEMPERATURE];
const HUMIDITY_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_HUMIDITY]; const HUMIDITY_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_HUMIDITY];
@customElement("dialog-area-registry-detail")
class DialogAreaDetail extends LitElement { class DialogAreaDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -426,5 +427,3 @@ declare global {
"dialog-area-registry-detail": DialogAreaDetail; "dialog-area-registry-detail": DialogAreaDetail;
} }
} }
customElements.define("dialog-area-registry-detail", DialogAreaDetail);

View File

@@ -0,0 +1,494 @@
import { mdiClose, mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import {
type AreasFloorHierarchy,
getAreasFloorHierarchy,
getAreasOrder,
getFloorOrder,
} from "../../../common/areas/areas-floor-hierarchy";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-dialog-header";
import "../../../components/ha-floor-icon";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../components/ha-md-dialog";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import {
reorderAreaRegistryEntries,
updateAreaRegistryEntry,
} from "../../../data/area_registry";
import { reorderFloorRegistryEntries } from "../../../data/floor_registry";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast";
import type { AreasFloorsOrderDialogParams } from "./show-dialog-areas-floors-order";
const UNASSIGNED_FLOOR = "__unassigned__";
interface FloorChange {
areaId: string;
floorId: string | null;
}
@customElement("dialog-areas-floors-order")
class DialogAreasFloorsOrder extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _hierarchy?: AreasFloorHierarchy;
@state() private _saving = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(
_params: AreasFloorsOrderDialogParams
): Promise<void> {
this._open = true;
this._computeHierarchy();
}
private _computeHierarchy(): void {
this._hierarchy = getAreasFloorHierarchy(
Object.values(this.hass.floors),
Object.values(this.hass.areas)
);
}
public closeDialog(): void {
this._dialog?.close();
}
private _dialogClosed(): void {
this._open = false;
this._hierarchy = undefined;
this._saving = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._open || !this._hierarchy) {
return nothing;
}
const dialogTitle = this.hass.localize(
"ui.panel.config.areas.dialog.reorder_title"
);
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title" .title=${dialogTitle}>${dialogTitle}</span>
</ha-dialog-header>
<div slot="content" class="content">
<ha-sortable
handle-selector=".floor-handle"
draggable-selector=".floor"
@item-moved=${this._floorMoved}
invert-swap
>
<div class="floors">
${repeat(
this._hierarchy.floors,
(floor) => floor.id,
(floor) => this._renderFloor(floor)
)}
</div>
</ha-sortable>
${this._renderUnassignedAreas()}
</div>
<div slot="actions">
<ha-button @click=${this.closeDialog} appearance="plain">
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._save} .disabled=${this._saving}>
${this.hass.localize("ui.common.save")}
</ha-button>
</div>
</ha-md-dialog>
`;
}
private _renderFloor(floor: { id: string; areas: string[] }) {
const floorEntry = this.hass.floors[floor.id];
if (!floorEntry) {
return nothing;
}
return html`
<div class="floor">
<div class="floor-header">
<ha-floor-icon .floor=${floorEntry}></ha-floor-icon>
<span class="floor-name">${floorEntry.name}</span>
<ha-svg-icon
class="floor-handle"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div>
<ha-sortable
handle-selector=".area-handle"
draggable-selector="ha-md-list-item"
@item-moved=${this._areaMoved}
@item-added=${this._areaAdded}
group="areas"
.floor=${floor.id}
>
<ha-md-list>
${floor.areas.length > 0
? floor.areas.map((areaId) => this._renderArea(areaId))
: html`<p class="empty">
${this.hass.localize(
"ui.panel.config.areas.dialog.empty_floor"
)}
</p>`}
</ha-md-list>
</ha-sortable>
</div>
`;
}
private _renderUnassignedAreas() {
const hasFloors = this._hierarchy!.floors.length > 0;
return html`
<div class="floor unassigned">
${hasFloors
? html`<div class="floor-header">
<span class="floor-name">
${this.hass.localize(
"ui.panel.config.areas.dialog.unassigned_areas"
)}
</span>
</div>`
: nothing}
<ha-sortable
handle-selector=".area-handle"
draggable-selector="ha-md-list-item"
@item-moved=${this._areaMoved}
@item-added=${this._areaAdded}
group="areas"
.floor=${UNASSIGNED_FLOOR}
>
<ha-md-list>
${this._hierarchy!.areas.length > 0
? this._hierarchy!.areas.map((areaId) => this._renderArea(areaId))
: html`<p class="empty">
${this.hass.localize(
"ui.panel.config.areas.dialog.empty_unassigned"
)}
</p>`}
</ha-md-list>
</ha-sortable>
</div>
`;
}
private _renderArea(areaId: string) {
const area = this.hass.areas[areaId];
if (!area) {
return nothing;
}
return html`
<ha-md-list-item .sortableData=${area}>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<span slot="headline">${area.name}</span>
<ha-svg-icon
class="area-handle"
slot="end"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</ha-md-list-item>
`;
}
private _floorMoved(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._hierarchy) {
return;
}
const { oldIndex, newIndex } = ev.detail;
const newFloors = [...this._hierarchy.floors];
const [movedFloor] = newFloors.splice(oldIndex, 1);
newFloors.splice(newIndex, 0, movedFloor);
this._hierarchy = {
...this._hierarchy,
floors: newFloors,
};
}
private _areaMoved(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._hierarchy) {
return;
}
const { floor } = ev.currentTarget as HTMLElement & { floor: string };
const { oldIndex, newIndex } = ev.detail;
const floorId = floor === UNASSIGNED_FLOOR ? null : floor;
if (floorId === null) {
// Reorder unassigned areas
const newAreas = [...this._hierarchy.areas];
const [movedArea] = newAreas.splice(oldIndex, 1);
newAreas.splice(newIndex, 0, movedArea);
this._hierarchy = {
...this._hierarchy,
areas: newAreas,
};
} else {
// Reorder areas within a floor
this._hierarchy = {
...this._hierarchy,
floors: this._hierarchy.floors.map((f) => {
if (f.id === floorId) {
const newAreas = [...f.areas];
const [movedArea] = newAreas.splice(oldIndex, 1);
newAreas.splice(newIndex, 0, movedArea);
return { ...f, areas: newAreas };
}
return f;
}),
};
}
}
private _areaAdded(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._hierarchy) {
return;
}
const { floor } = ev.currentTarget as HTMLElement & { floor: string };
const { data: area, index } = ev.detail as {
data: AreaRegistryEntry;
index: number;
};
const newFloorId = floor === UNASSIGNED_FLOOR ? null : floor;
// Update hierarchy
const newUnassignedAreas = this._hierarchy.areas.filter(
(id) => id !== area.area_id
);
if (newFloorId === null) {
// Add to unassigned at the specified index
newUnassignedAreas.splice(index, 0, area.area_id);
}
this._hierarchy = {
...this._hierarchy,
floors: this._hierarchy.floors.map((f) => {
if (f.id === newFloorId) {
// Add to new floor at the specified index
const newAreas = [...f.areas];
newAreas.splice(index, 0, area.area_id);
return { ...f, areas: newAreas };
}
// Remove from old floor
return {
...f,
areas: f.areas.filter((id) => id !== area.area_id),
};
}),
areas: newUnassignedAreas,
};
}
private _computeFloorChanges(): FloorChange[] {
if (!this._hierarchy) {
return [];
}
const changes: FloorChange[] = [];
// Check areas assigned to floors
for (const floor of this._hierarchy.floors) {
for (const areaId of floor.areas) {
const originalFloorId = this.hass.areas[areaId]?.floor_id ?? null;
if (floor.id !== originalFloorId) {
changes.push({ areaId, floorId: floor.id });
}
}
}
// Check unassigned areas
for (const areaId of this._hierarchy.areas) {
const originalFloorId = this.hass.areas[areaId]?.floor_id ?? null;
if (originalFloorId !== null) {
changes.push({ areaId, floorId: null });
}
}
return changes;
}
private async _save(): Promise<void> {
if (!this._hierarchy || this._saving) {
return;
}
this._saving = true;
try {
const areaOrder = getAreasOrder(this._hierarchy);
const floorOrder = getFloorOrder(this._hierarchy);
// Update floor assignments for areas that changed floors
const floorChanges = this._computeFloorChanges();
const floorChangePromises = floorChanges.map(({ areaId, floorId }) =>
updateAreaRegistryEntry(this.hass, areaId, {
floor_id: floorId,
})
);
await Promise.all(floorChangePromises);
// Reorder areas and floors
await reorderAreaRegistryEntries(this.hass, areaOrder);
await reorderFloorRegistryEntries(this.hass, floorOrder);
this.closeDialog();
} catch (err: any) {
showToast(this, {
message:
err.message ||
this.hass.localize("ui.panel.config.areas.dialog.reorder_failed"),
});
this._saving = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-md-dialog {
min-width: 600px;
max-height: 90%;
--dialog-content-padding: 8px 24px;
}
@media all and (max-width: 600px), all and (max-height: 500px) {
ha-md-dialog {
--md-dialog-container-shape: 0;
min-width: 100%;
min-height: 100%;
}
}
.floors {
display: flex;
flex-direction: column;
gap: 16px;
}
.floor {
border: 1px solid var(--divider-color);
border-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
overflow: hidden;
}
.floor.unassigned {
border-style: dashed;
margin-top: 16px;
}
.floor-header {
display: flex;
align-items: center;
padding: 12px 16px;
background-color: var(--secondary-background-color);
gap: 12px;
}
.floor-name {
flex: 1;
font-weight: var(--ha-font-weight-medium);
}
.floor-handle {
cursor: grab;
color: var(--secondary-text-color);
}
ha-md-list {
padding: 0;
--md-list-item-leading-space: 16px;
--md-list-item-trailing-space: 16px;
display: flex;
flex-direction: column;
}
ha-md-list-item {
--md-list-item-one-line-container-height: 48px;
--md-list-item-container-shape: 0;
}
ha-md-list-item.sortable-ghost {
border-radius: calc(
var(--ha-card-border-radius, var(--ha-border-radius-lg)) - 1px
);
box-shadow: inset 0 0 0 2px var(--primary-color);
}
.area-handle {
cursor: grab;
color: var(--secondary-text-color);
}
.empty {
text-align: center;
color: var(--secondary-text-color);
font-style: italic;
margin: 0;
padding: 12px 16px;
order: 1;
}
ha-md-list:has(ha-md-list-item) .empty {
display: none;
}
.content {
padding-top: 16px;
padding-bottom: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-areas-floors-order": DialogAreasFloorsOrder;
}
}

View File

@@ -1,7 +1,7 @@
import { mdiTextureBox } from "@mdi/js"; import { mdiTextureBox } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
@@ -27,6 +27,7 @@ import type { HomeAssistant } from "../../../types";
import { showAreaRegistryDetailDialog } from "./show-dialog-area-registry-detail"; import { showAreaRegistryDetailDialog } from "./show-dialog-area-registry-detail";
import type { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail"; import type { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail";
@customElement("dialog-floor-registry-detail")
class DialogFloorDetail extends LitElement { class DialogFloorDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -144,6 +145,10 @@ class DialogFloorDetail extends LitElement {
"ui.panel.config.floors.editor.level" "ui.panel.config.floors.editor.level"
)} )}
type="number" type="number"
.helper=${this.hass.localize(
"ui.panel.config.floors.editor.level_helper"
)}
helperPersistent
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
@@ -357,5 +362,3 @@ declare global {
"dialog-floor-registry-detail": DialogFloorDetail; "dialog-floor-registry-detail": DialogFloorDetail;
} }
} }
customElements.define("dialog-floor-registry-detail", DialogFloorDetail);

View File

@@ -2,10 +2,10 @@ import type { ActionDetail } from "@material/mwc-list";
import { import {
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiDragHorizontalVariant,
mdiHelpCircle, mdiHelpCircle,
mdiPencil, mdiPencil,
mdiPlus, mdiPlus,
mdiSort,
} from "@mdi/js"; } from "@mdi/js";
import { import {
css, css,
@@ -21,7 +21,6 @@ import memoizeOne from "memoize-one";
import { import {
getAreasFloorHierarchy, getAreasFloorHierarchy,
getAreasOrder, getAreasOrder,
getFloorOrder,
type AreasFloorHierarchy, type AreasFloorHierarchy,
} from "../../../common/areas/areas-floor-hierarchy"; } from "../../../common/areas/areas-floor-hierarchy";
import { formatListWithAnds } from "../../../common/string/format-list"; import { formatListWithAnds } from "../../../common/string/format-list";
@@ -42,7 +41,6 @@ import type { FloorRegistryEntry } from "../../../data/floor_registry";
import { import {
createFloorRegistryEntry, createFloorRegistryEntry,
deleteFloorRegistryEntry, deleteFloorRegistryEntry,
reorderFloorRegistryEntries,
updateFloorRegistryEntry, updateFloorRegistryEntry,
} from "../../../data/floor_registry"; } from "../../../data/floor_registry";
import { import {
@@ -58,6 +56,7 @@ import {
loadAreaRegistryDetailDialog, loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog, showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail"; } from "./show-dialog-area-registry-detail";
import { showAreasFloorsOrderDialog } from "./show-dialog-areas-floors-order";
import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail"; import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail";
const UNASSIGNED_FLOOR = "__unassigned__"; const UNASSIGNED_FLOOR = "__unassigned__";
@@ -84,6 +83,8 @@ export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
private _searchParms = new URLSearchParams(window.location.search);
@state() private _hierarchy?: AreasFloorHierarchy; @state() private _hierarchy?: AreasFloorHierarchy;
private _blockHierarchyUpdate = false; private _blockHierarchyUpdate = false;
@@ -167,99 +168,97 @@ export class HaConfigAreasDashboard extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.isWide=${this.isWide} .isWide=${this.isWide}
back-path="/config" .backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.tabs=${configSections.areas} .tabs=${configSections.areas}
.route=${this.route} .route=${this.route}
has-fab has-fab
> >
<ha-icon-button <ha-button-menu slot="toolbar-icon">
slot="toolbar-icon" <ha-icon-button
.label=${this.hass.localize("ui.common.help")} slot="trigger"
.path=${mdiHelpCircle} .label=${this.hass.localize("ui.common.menu")}
@click=${this._showHelp} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
<ha-list-item graphic="icon" @click=${this._showReorderDialog}>
<ha-svg-icon .path=${mdiSort} slot="graphic"></ha-svg-icon>
${this.hass.localize("ui.panel.config.areas.picker.reorder")}
</ha-list-item>
<ha-list-item graphic="icon" @click=${this._showHelp}>
<ha-svg-icon .path=${mdiHelpCircle} slot="graphic"></ha-svg-icon>
${this.hass.localize("ui.common.help")}
</ha-list-item>
</ha-button-menu>
<div class="container"> <div class="container">
<ha-sortable <div class="floors">
handle-selector=".handle" ${this._hierarchy.floors.map(({ areas, id }) => {
draggable-selector=".floor" const floor = this.hass.floors[id];
@item-moved=${this._floorMoved} if (!floor) {
.options=${SORT_OPTIONS} return nothing;
group="floors" }
invert-swap return html`
> <div class="floor">
<div class="floors"> <div class="header">
${this._hierarchy.floors.map(({ areas, id }) => { <h2>
const floor = this.hass.floors[id]; <ha-floor-icon .floor=${floor}></ha-floor-icon>
if (!floor) { ${floor.name}
return nothing; </h2>
} <div class="actions">
return html` <ha-button-menu
<div class="floor"> .floor=${floor}
<div class="header"> @action=${this._handleFloorAction}
<h2> >
<ha-floor-icon .floor=${floor}></ha-floor-icon> <ha-icon-button
${floor.name} slot="trigger"
</h2> .path=${mdiDotsVertical}
<div class="actions"> ></ha-icon-button>
<ha-svg-icon <ha-list-item graphic="icon"
class="handle" ><ha-svg-icon
.path=${mdiDragHorizontalVariant} .path=${mdiPencil}
></ha-svg-icon> slot="graphic"
<ha-button-menu ></ha-svg-icon
.floor=${floor} >${this.hass.localize(
@action=${this._handleFloorAction} "ui.panel.config.areas.picker.floor.edit_floor"
)}</ha-list-item
> >
<ha-icon-button <ha-list-item class="warning" graphic="icon"
slot="trigger" ><ha-svg-icon
.path=${mdiDotsVertical} class="warning"
></ha-icon-button> .path=${mdiDelete}
<ha-list-item graphic="icon" slot="graphic"
><ha-svg-icon ></ha-svg-icon
.path=${mdiPencil} >${this.hass.localize(
slot="graphic" "ui.panel.config.areas.picker.floor.delete_floor"
></ha-svg-icon )}</ha-list-item
>${this.hass.localize( >
"ui.panel.config.areas.picker.floor.edit_floor" </ha-button-menu>
)}</ha-list-item
>
<ha-list-item class="warning" graphic="icon"
><ha-svg-icon
class="warning"
.path=${mdiDelete}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.delete_floor"
)}</ha-list-item
>
</ha-button-menu>
</div>
</div> </div>
<ha-sortable
handle-selector="a"
draggable-selector="a"
@item-added=${this._areaAdded}
@item-moved=${this._areaMoved}
group="areas"
.options=${SORT_OPTIONS}
.floor=${floor.floor_id}
>
<div class="areas">
${areas.map((areaId) => {
const area = this.hass.areas[areaId];
if (!area) {
return nothing;
}
const stats = areasStats.get(area.area_id);
return this._renderArea(area, stats);
})}
</div>
</ha-sortable>
</div> </div>
`; <ha-sortable
})} handle-selector="a"
</div> draggable-selector="a"
</ha-sortable> @item-added=${this._areaAdded}
@item-moved=${this._areaMoved}
group="areas"
.options=${SORT_OPTIONS}
.floor=${floor.floor_id}
>
<div class="areas">
${areas.map((areaId) => {
const area = this.hass.areas[areaId];
if (!area) {
return nothing;
}
const stats = areasStats.get(area.area_id);
return this._renderArea(area, stats);
})}
</div>
</ha-sortable>
</div>
`;
})}
</div>
${this._hierarchy.areas.length ${this._hierarchy.areas.length
? html` ? html`
@@ -391,51 +390,6 @@ export class HaConfigAreasDashboard extends LitElement {
}); });
} }
private async _floorMoved(ev) {
ev.stopPropagation();
if (!this.hass || !this._hierarchy) {
return;
}
const { oldIndex, newIndex } = ev.detail;
const reorderFloors = (
floors: AreasFloorHierarchy["floors"],
oldIdx: number,
newIdx: number
) => {
const newFloors = [...floors];
const [movedFloor] = newFloors.splice(oldIdx, 1);
newFloors.splice(newIdx, 0, movedFloor);
return newFloors;
};
// Optimistically update UI
this._hierarchy = {
...this._hierarchy,
floors: reorderFloors(this._hierarchy.floors, oldIndex, newIndex),
};
const areaOrder = getAreasOrder(this._hierarchy);
const floorOrder = getFloorOrder(this._hierarchy);
// Block hierarchy updates for 500ms to avoid flickering
// because of multiple async updates
this._blockHierarchyUpdateFor(500);
try {
await reorderAreaRegistryEntries(this.hass, areaOrder);
await reorderFloorRegistryEntries(this.hass, floorOrder);
} catch {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.areas.picker.floor_reorder_failed"
),
});
// Revert on error
this._computeHierarchy();
}
}
private async _areaMoved(ev) { private async _areaMoved(ev) {
ev.stopPropagation(); ev.stopPropagation();
if (!this.hass || !this._hierarchy) { if (!this.hass || !this._hierarchy) {
@@ -598,6 +552,10 @@ export class HaConfigAreasDashboard extends LitElement {
this._openAreaDialog(); this._openAreaDialog();
} }
private _showReorderDialog() {
showAreasFloorsOrderDialog(this, {});
}
private _showHelp() { private _showHelp() {
showAlertDialog(this, { showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.areas.caption"), title: this.hass.localize("ui.panel.config.areas.caption"),

View File

@@ -0,0 +1,17 @@
import { fireEvent } from "../../../common/dom/fire_event";
export interface AreasFloorsOrderDialogParams {}
export const loadAreasFloorsOrderDialog = () =>
import("./dialog-areas-floors-order");
export const showAreasFloorsOrderDialog = (
element: HTMLElement,
params: AreasFloorsOrderDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-areas-floors-order",
dialogImport: loadAreasFloorsOrderDialog,
dialogParams: params,
});
};

View File

@@ -16,6 +16,7 @@ import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { mainWindow } from "../../../common/dom/get_main_window";
import { computeAreaName } from "../../../common/entity/compute_area_name"; import { computeAreaName } from "../../../common/entity/compute_area_name";
import { computeDeviceName } from "../../../common/entity/compute_device_name"; import { computeDeviceName } from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
@@ -114,12 +115,10 @@ import {
} from "../../../data/trigger"; } from "../../../data/trigger";
import type { HassDialog } from "../../../dialogs/make-dialog-manager"; import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { isMac } from "../../../util/is_mac"; import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import "./add-automation-element/ha-automation-add-from-target"; import "./add-automation-element/ha-automation-add-from-target";
import type HaAutomationAddFromTarget from "./add-automation-element/ha-automation-add-from-target";
import "./add-automation-element/ha-automation-add-items"; import "./add-automation-element/ha-automation-add-items";
import "./add-automation-element/ha-automation-add-search"; import "./add-automation-element/ha-automation-add-search";
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog"; import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
@@ -168,7 +167,7 @@ const DYNAMIC_KEYWORDS = ["dynamicGroups", "helpers", "other"];
@customElement("add-automation-element-dialog") @customElement("add-automation-element-dialog")
class DialogAddAutomationElement class DialogAddAutomationElement
extends KeyboardShortcutMixin(SubscribeMixin(LitElement)) extends KeyboardShortcutMixin(LitElement)
implements HassDialog implements HassDialog
{ {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -217,10 +216,6 @@ class DialogAddAutomationElement
// #endregion state // #endregion state
// #region queries // #region queries
@query("ha-automation-add-from-target")
private _targetPickerElement?: HaAutomationAddFromTarget;
@query("ha-automation-add-items") @query("ha-automation-add-items")
private _itemsListElement?: HTMLDivElement; private _itemsListElement?: HTMLDivElement;
@@ -233,6 +228,8 @@ class DialogAddAutomationElement
private _unsub?: Promise<UnsubscribeFunc>; private _unsub?: Promise<UnsubscribeFunc>;
private _unsubscribeLabFeatures?: UnsubscribeFunc;
private _configEntryLookup: Record<string, ConfigEntry> = {}; private _configEntryLookup: Record<string, ConfigEntry> = {};
// #endregion variables // #endregion variables
@@ -246,33 +243,36 @@ class DialogAddAutomationElement
) { ) {
this._calculateUsedDomains(); this._calculateUsedDomains();
} }
if (changedProps.has("_newTriggersAndConditions")) {
this._subscribeDescriptions();
}
} }
public hassSubscribe() { private _subscribeDescriptions() {
return [ this._unsubscribe();
subscribeLabFeatures(this.hass!.connection, (features) => { if (this._params?.type === "trigger") {
this._newTriggersAndConditions = this._triggerDescriptions = {};
features.find( this._unsub = subscribeTriggers(this.hass, (triggers) => {
(feature) => this._triggerDescriptions = {
feature.domain === "automation" && ...this._triggerDescriptions,
feature.preview_feature === "new_triggers_conditions" ...triggers,
)?.enabled ?? false; };
this._tab = });
this._newTriggersAndConditions && this._params?.type !== "condition" } else if (this._params?.type === "condition") {
? "targets" this._conditionDescriptions = {};
: "groups"; this._unsub = subscribeConditions(this.hass, (conditions) => {
}), this._conditionDescriptions = {
]; ...this._conditionDescriptions,
...conditions,
};
});
}
} }
public showDialog(params): void { public showDialog(params): void {
this._params = params; this._params = params;
this._tab =
this._newTriggersAndConditions && this._params?.type !== "condition"
? "targets"
: "groups";
this.addKeyboardShortcuts(); this.addKeyboardShortcuts();
this._loadConfigEntries(); this._loadConfigEntries();
@@ -281,27 +281,38 @@ class DialogAddAutomationElement
this._fetchManifests(); this._fetchManifests();
this._calculateUsedDomains(); this._calculateUsedDomains();
this._unsubscribeLabFeatures = subscribeLabFeatures(
this.hass.connection,
(features) => {
this._newTriggersAndConditions =
features.find(
(feature) =>
feature.domain === "automation" &&
feature.preview_feature === "new_triggers_conditions"
)?.enabled ?? false;
this._tab = this._newTriggersAndConditions ? "targets" : "groups";
}
);
// add initial dialog view state to history
mainWindow.history.pushState(
{
dialogData: {},
},
""
);
if (this._params?.type === "action") { if (this._params?.type === "action") {
this.hass.loadBackendTranslation("services"); this.hass.loadBackendTranslation("services");
getServiceIcons(this.hass); getServiceIcons(this.hass);
} else if (this._params?.type === "trigger") { } else if (this._params?.type === "trigger") {
this.hass.loadBackendTranslation("triggers"); this.hass.loadBackendTranslation("triggers");
getTriggerIcons(this.hass); getTriggerIcons(this.hass);
this._unsub = subscribeTriggers(this.hass, (triggers) => { this._subscribeDescriptions();
this._triggerDescriptions = {
...this._triggerDescriptions,
...triggers,
};
});
} else if (this._params?.type === "condition") { } else if (this._params?.type === "condition") {
this.hass.loadBackendTranslation("conditions"); this.hass.loadBackendTranslation("conditions");
getConditionIcons(this.hass); getConditionIcons(this.hass);
this._unsub = subscribeConditions(this.hass, (conditions) => { this._subscribeDescriptions();
this._conditionDescriptions = {
...this._conditionDescriptions,
...conditions,
};
});
} }
window.addEventListener("resize", this._updateNarrow); window.addEventListener("resize", this._updateNarrow);
@@ -311,7 +322,41 @@ class DialogAddAutomationElement
this._bottomSheetMode = this._narrow; this._bottomSheetMode = this._narrow;
} }
public closeDialog() { public closeDialog(historyState?: any) {
// prevent closing when come from popstate event and root level isn't active
if (
this._open &&
historyState &&
(this._selectedTarget || this._selectedGroup)
) {
if (historyState.dialogData?.target) {
this._selectedTarget = historyState.dialogData.target;
this._getItemsByTarget();
this._tab = "targets";
return false;
}
if (historyState.dialogData?.group) {
this._selectedCollectionIndex = historyState.dialogData.collectionIndex;
this._selectedGroup = historyState.dialogData.group;
this._tab = "groups";
return false;
}
// return to home on mobile
if (this._narrow) {
this._selectedTarget = undefined;
this._selectedGroup = undefined;
return false;
}
}
// if dialog is closed, but root level isn't active, clean up history state
if (mainWindow.history.state?.dialogData) {
this._open = false;
mainWindow.history.back();
return false;
}
this.removeKeyboardShortcuts(); this.removeKeyboardShortcuts();
this._unsubscribe(); this._unsubscribe();
if (this._params) { if (this._params) {
@@ -379,6 +424,10 @@ class DialogAddAutomationElement
this._unsub.then((unsub) => unsub()); this._unsub.then((unsub) => unsub());
this._unsub = undefined; this._unsub = undefined;
} }
if (this._unsubscribeLabFeatures) {
this._unsubscribeLabFeatures();
this._unsubscribeLabFeatures = undefined;
}
} }
// #endregion lifecycle // #endregion lifecycle
@@ -394,7 +443,7 @@ class DialogAddAutomationElement
return html` return html`
<ha-bottom-sheet <ha-bottom-sheet
.open=${this._open} .open=${this._open}
@closed=${this.closeDialog} @closed=${this._handleClosed}
flexcontent flexcontent
> >
${this._renderContent()} ${this._renderContent()}
@@ -406,7 +455,7 @@ class DialogAddAutomationElement
<ha-wa-dialog <ha-wa-dialog
width="large" width="large"
.open=${this._open} .open=${this._open}
@closed=${this.closeDialog} @closed=${this._handleClosed}
flexcontent flexcontent
> >
${this._renderContent()} ${this._renderContent()}
@@ -426,10 +475,7 @@ class DialogAddAutomationElement
}, },
]; ];
if ( if (this._newTriggersAndConditions) {
this._newTriggersAndConditions &&
automationElementType !== "condition"
) {
tabButtons.unshift({ tabButtons.unshift({
label: this.hass.localize(`ui.panel.config.automation.editor.targets`), label: this.hass.localize(`ui.panel.config.automation.editor.targets`),
value: "targets", value: "targets",
@@ -519,8 +565,7 @@ class DialogAddAutomationElement
this._manifests this._manifests
)} )}
.convertToItem=${this._convertToItem} .convertToItem=${this._convertToItem}
.newTriggersAndConditions=${this._newTriggersAndConditions && .newTriggersAndConditions=${this._newTriggersAndConditions}
automationElementType !== "condition"}
@search-element-picked=${this._searchItemSelected} @search-element-picked=${this._searchItemSelected}
> >
</ha-automation-add-search>` </ha-automation-add-search>`
@@ -548,8 +593,7 @@ class DialogAddAutomationElement
interactive interactive
type="button" type="button"
class="paste" class="paste"
.value=${PASTE_VALUE} @click=${this._paste}
@click=${this._selected}
> >
<div class="shortcut-label"> <div class="shortcut-label">
<div class="label"> <div class="label">
@@ -599,7 +643,7 @@ class DialogAddAutomationElement
: nothing} : nothing}
${collections.map( ${collections.map(
(collection, index) => html` (collection, index) => html`
${collection.titleKey ${collection.titleKey && collection.groups.length
? html`<ha-section-title> ? html`<ha-section-title>
${this.hass.localize(collection.titleKey)} ${this.hass.localize(collection.titleKey)}
</ha-section-title>` </ha-section-title>`
@@ -720,15 +764,26 @@ class DialogAddAutomationElement
); );
if (targetId) { if (targetId) {
if (targetType === "area" && this.hass.areas[targetId]?.floor_id) { if (targetType === "area") {
const floorId = this.hass.areas[targetId].floor_id; const floorId = this.hass.areas[targetId]?.floor_id;
subtitle = computeFloorName(this.hass.floors[floorId]) || floorId; if (floorId) {
} subtitle = computeFloorName(this.hass.floors[floorId]) || floorId;
if (targetType === "device" && this.hass.devices[targetId]?.area_id) { } else {
const areaId = this.hass.devices[targetId].area_id; subtitle = this.hass.localize(
subtitle = computeAreaName(this.hass.areas[areaId]) || areaId; "ui.panel.config.automation.editor.other_areas"
} );
if (targetType === "entity" && this.hass.states[targetId]) { }
} else if (targetType === "device") {
const areaId = this.hass.devices[targetId]?.area_id;
if (areaId) {
subtitle = computeAreaName(this.hass.areas[areaId]) || areaId;
} else {
const device = this.hass.devices[targetId];
subtitle = this.hass.localize(
`ui.panel.config.automation.editor.${device?.entry_type === "service" ? "services" : "unassigned_devices"}`
);
}
} else if (targetType === "entity" && this.hass.states[targetId]) {
const entity = this.hass.entities[targetId]; const entity = this.hass.entities[targetId];
if (entity && !entity.device_id && !entity.area_id) { if (entity && !entity.device_id && !entity.area_id) {
const domain = targetId.split(".", 2)[0]; const domain = targetId.split(".", 2)[0];
@@ -753,10 +808,10 @@ class DialogAddAutomationElement
.join(computeRTL(this.hass) ? " ◂ " : " ▸ "); .join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
} }
} }
}
if (subtitle) { if (subtitle) {
return html`<span slot="subtitle">${subtitle}</span>`; return html`<span slot="subtitle">${subtitle}</span>`;
}
} }
} }
@@ -1343,6 +1398,61 @@ class DialogAddAutomationElement
this._labelRegistry?.find(({ label_id }) => label_id === labelId) this._labelRegistry?.find(({ label_id }) => label_id === labelId)
); );
private _getDomainType(domain: string) {
return ENTITY_DOMAINS_MAIN.has(domain) ||
(this._manifests?.[domain].integration_type === "entity" &&
!ENTITY_DOMAINS_OTHER.has(domain))
? "dynamicGroups"
: this._manifests?.[domain].integration_type === "helper"
? "helpers"
: "other";
}
private _sortDomainsByCollection(
type: AddAutomationElementDialogParams["type"],
entries: [
string,
{ title: string; items: AddAutomationElementListItem[] },
][]
): { title: string; items: AddAutomationElementListItem[] }[] {
const order: string[] = [];
TYPES[type].collections.forEach((collection) => {
order.push(...Object.keys(collection.groups));
});
return entries
.sort((a, b) => {
const domainA = a[0];
const domainB = b[0];
if (order.includes(domainA) && order.includes(domainB)) {
return order.indexOf(domainA) - order.indexOf(domainB);
}
let typeA = domainA;
let typeB = domainB;
if (!order.includes(domainA)) {
typeA = this._getDomainType(domainA);
}
if (!order.includes(domainB)) {
typeB = this._getDomainType(domainB);
}
if (typeA === typeB) {
return stringCompare(
a[1].title,
b[1].title,
this.hass.locale.language
);
}
return order.indexOf(typeA) - order.indexOf(typeB);
})
.map((entry) => entry[1]);
}
// #endregion data // #endregion data
// #region data memoize // #region data memoize
@@ -1358,12 +1468,12 @@ class DialogAddAutomationElement
private _getAreaEntityLookupMemoized = memoizeOne( private _getAreaEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) => (entities: HomeAssistant["entities"]) =>
getAreaEntityLookup(Object.values(entities), true) getAreaEntityLookup(Object.values(entities))
); );
private _getDeviceEntityLookupMemoized = memoizeOne( private _getDeviceEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) => (entities: HomeAssistant["entities"]) =>
getDeviceEntityLookup(Object.values(entities), true) getDeviceEntityLookup(Object.values(entities))
); );
private _extractTypeAndIdFromTarget = memoizeOne( private _extractTypeAndIdFromTarget = memoizeOne(
@@ -1428,8 +1538,9 @@ class DialogAddAutomationElement
); );
}); });
return Object.values(items).sort((a, b) => return this._sortDomainsByCollection(
stringCompare(a.title, b.title, this.hass.locale.language) this._params!.type,
Object.entries(items)
); );
} }
@@ -1538,8 +1649,9 @@ class DialogAddAutomationElement
); );
}); });
return Object.values(items).sort((a, b) => return this._sortDomainsByCollection(
stringCompare(a.title, b.title, this.hass.locale.language) this._params!.type,
Object.entries(items)
); );
} }
@@ -1570,8 +1682,9 @@ class DialogAddAutomationElement
); );
}); });
return Object.values(items).sort((a, b) => return this._sortDomainsByCollection(
stringCompare(a.title, b.title, this.hass.locale.language) this._params!.type,
Object.entries(items)
); );
} }
@@ -1584,11 +1697,7 @@ class DialogAddAutomationElement
} }
private _back() { private _back() {
if (this._selectedTarget) { mainWindow.history.back();
this._targetPickerElement?.navigateBack();
return;
}
this._selectedGroup = undefined;
} }
private _groupSelected(ev) { private _groupSelected(ev) {
@@ -1600,11 +1709,26 @@ class DialogAddAutomationElement
} }
this._selectedGroup = group.value; this._selectedGroup = group.value;
this._selectedCollectionIndex = ev.currentTarget.index; this._selectedCollectionIndex = ev.currentTarget.index;
mainWindow.history.pushState(
{
dialogData: {
group: this._selectedGroup,
collectionIndex: this._selectedCollectionIndex,
},
},
""
);
requestAnimationFrame(() => { requestAnimationFrame(() => {
this._itemsListElement?.scrollTo(0, 0); this._itemsListElement?.scrollTo(0, 0);
}); });
} }
private _paste() {
this._params!.add(PASTE_VALUE);
this.closeDialog();
}
private _selected(ev: CustomEvent<{ value: string }>) { private _selected(ev: CustomEvent<{ value: string }>) {
let target: HassServiceTarget | undefined; let target: HassServiceTarget | undefined;
if ( if (
@@ -1624,6 +1748,14 @@ class DialogAddAutomationElement
this._targetItems = undefined; this._targetItems = undefined;
this._loadItemsError = false; this._loadItemsError = false;
this._selectedTarget = ev.detail.value; this._selectedTarget = ev.detail.value;
mainWindow.history.pushState(
{
dialogData: {
target: this._selectedTarget,
},
},
""
);
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (this._narrow) { if (this._narrow) {
@@ -1668,14 +1800,19 @@ class DialogAddAutomationElement
} }
if (this._params!.type === "action") { if (this._params!.type === "action") {
const items = await getServicesForTarget( const items: string[] = await getServicesForTarget(
this.hass.callWS, this.hass.callWS,
this._selectedTarget this._selectedTarget
); );
const filteredItems = items.filter(
// homeassistant services are too generic to be applied on the selected target
(service) => !service.startsWith("homeassistant.")
);
this._targetItems = this._getDomainGroupedActionListItems( this._targetItems = this._getDomainGroupedActionListItems(
this.hass.localize, this.hass.localize,
items filteredItems
); );
} }
} catch (err) { } catch (err) {
@@ -1738,6 +1875,10 @@ class DialogAddAutomationElement
this._tab = "targets"; this._tab = "targets";
} }
private _handleClosed() {
this.closeDialog();
}
// #region interaction // #region interaction
// #region render helpers // #region render helpers
@@ -1903,7 +2044,7 @@ class DialogAddAutomationElement
ha-wa-dialog { ha-wa-dialog {
--dialog-content-padding: var(--ha-space-0); --dialog-content-padding: var(--ha-space-0);
--ha-dialog-min-height: min( --ha-dialog-min-height: min(
648px, 800px,
calc( calc(
100vh - max( 100vh - max(
var(--safe-area-inset-bottom), var(--safe-area-inset-bottom),
@@ -1912,7 +2053,7 @@ class DialogAddAutomationElement
) )
); );
--ha-dialog-min-height: min( --ha-dialog-min-height: min(
648px, 800px,
calc( calc(
100dvh - max( 100dvh - max(
var(--safe-area-inset-bottom), var(--safe-area-inset-bottom),

View File

@@ -553,9 +553,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
area.icon, area.icon,
] as [string, string, string | undefined, string | undefined]; ] as [string, string, string | undefined, string | undefined];
}) })
.sort(([, nameA], [, nameB]) =>
stringCompare(nameA, nameB, this.hass.locale.language)
)
.map(([areaTargetId, areaName, floorId, areaIcon]) => { .map(([areaTargetId, areaName, floorId, areaIcon]) => {
const { open, devices, entities } = const { open, devices, entities } =
this._entries[`floor${TARGET_SEPARATOR}${floorId || ""}`].areas![ this._entries[`floor${TARGET_SEPARATOR}${floorId || ""}`].areas![
@@ -708,7 +705,11 @@ export default class HaAutomationAddFromTarget extends LitElement {
this.floors this.floors
); );
const label = entityName || deviceName || entityId; let label = entityName || deviceName || entityId;
if (this.entities[entityId]?.hidden) {
label += ` (${this.localize("ui.panel.config.automation.editor.entity_hidden")})`;
}
return [entityId, label, stateObj] as [string, string, HassEntity]; return [entityId, label, stateObj] as [string, string, HassEntity];
}) })
@@ -837,12 +838,12 @@ export default class HaAutomationAddFromTarget extends LitElement {
private _getAreaEntityLookupMemoized = memoizeOne( private _getAreaEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) => (entities: HomeAssistant["entities"]) =>
getAreaEntityLookup(Object.values(entities), true) getAreaEntityLookup(Object.values(entities))
); );
private _getDeviceEntityLookupMemoized = memoizeOne( private _getDeviceEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) => (entities: HomeAssistant["entities"]) =>
getDeviceEntityLookup(Object.values(entities), true) getDeviceEntityLookup(Object.values(entities))
); );
private _getSelectedTargetId = memoizeOne( private _getSelectedTargetId = memoizeOne(
@@ -1382,92 +1383,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
); );
} }
public navigateBack() {
if (!this.value) {
return;
}
const valueType = Object.keys(this.value)[0].replace("_id", "");
const valueId = this.value[`${valueType}_id`];
if (
valueType === "floor" ||
valueType === "label" ||
(!valueId &&
(valueType === "device" ||
valueType === "helper" ||
valueType === "service" ||
valueType === "area"))
) {
fireEvent(this, "value-changed", { value: undefined });
return;
}
if (valueType === "area") {
fireEvent(this, "value-changed", {
value: { floor_id: this.areas[valueId].floor_id || undefined },
});
return;
}
if (valueType === "device") {
if (
!this.devices[valueId].area_id &&
this.devices[valueId].entry_type === "service"
) {
fireEvent(this, "value-changed", {
value: { service_id: undefined },
});
return;
}
fireEvent(this, "value-changed", {
value: { area_id: this.devices[valueId].area_id || undefined },
});
return;
}
if (valueType === "entity" && valueId) {
const deviceId = this.entities[valueId].device_id;
if (deviceId) {
fireEvent(this, "value-changed", {
value: { device_id: deviceId },
});
return;
}
const areaId = this.entities[valueId].area_id;
if (areaId) {
fireEvent(this, "value-changed", {
value: { area_id: areaId },
});
return;
}
const domain = valueId.split(".", 2)[0];
const manifest = this.manifests ? this.manifests[domain] : undefined;
if (manifest?.integration_type === "helper") {
fireEvent(this, "value-changed", {
value: { [`helper_${domain}_id`]: undefined },
});
return;
}
fireEvent(this, "value-changed", {
value: { [`entity_${domain}_id`]: undefined },
});
}
if (valueType.startsWith("helper_") || valueType.startsWith("entity_")) {
fireEvent(this, "value-changed", {
value: {
[`${valueType.startsWith("helper_") ? "helper" : "device"}_id`]:
undefined,
},
});
}
}
private _expandHeight() { private _expandHeight() {
this._fullHeight = true; this._fullHeight = true;
this.style.setProperty("--max-height", "none"); this.style.setProperty("--max-height", "none");

View File

@@ -273,7 +273,7 @@ export class HaAutomationAddItems extends LitElement {
align-items: center; align-items: center;
color: var(--ha-color-text-secondary); color: var(--ha-color-text-secondary);
padding: var(--ha-space-0); padding: var(--ha-space-0);
margin: var(--ha-space-3) var(--ha-space-4) margin: var(--ha-space-0) var(--ha-space-4)
max(var(--safe-area-inset-bottom), var(--ha-space-3)); max(var(--safe-area-inset-bottom), var(--ha-space-3));
line-height: var(--ha-line-height-expanded); line-height: var(--ha-line-height-expanded);
justify-content: center; justify-content: center;
@@ -306,7 +306,7 @@ export class HaAutomationAddItems extends LitElement {
.items .item-headline { .items .item-headline {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--ha-space-1); gap: var(--ha-space-2);
min-height: var(--ha-space-9); min-height: var(--ha-space-9);
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -366,12 +366,16 @@ export class HaAutomationAddItems extends LitElement {
} }
.selected-target state-badge { .selected-target state-badge {
--mdc-icon-size: 20px; --mdc-icon-size: 24px;
} }
.selected-target state-badge, .selected-target state-badge,
.selected-target ha-floor-icon {
display: flex;
height: 32px;
width: 32px;
align-items: center;
}
.selected-target ha-domain-icon { .selected-target ha-domain-icon {
width: 24px;
height: 24px;
filter: grayscale(100%); filter: grayscale(100%);
} }
`; `;

View File

@@ -1,6 +1,9 @@
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js"; import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import type { HassServiceTarget } from "home-assistant-js-websocket"; import type {
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators"; import { customElement, property, queryAll, state } from "lit/decorators";
@@ -25,6 +28,7 @@ import {
CONDITION_BUILDING_BLOCKS, CONDITION_BUILDING_BLOCKS,
subscribeConditions, subscribeConditions,
} from "../../../../data/condition"; } from "../../../../data/condition";
import { subscribeLabFeatures } from "../../../../data/labs";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { import {
@@ -74,19 +78,52 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
private _conditionKeys = new WeakMap<Condition, string>(); private _conditionKeys = new WeakMap<Condition, string>();
private _unsub?: Promise<UnsubscribeFunc>;
// @ts-ignore
@state() private _newTriggersAndConditions = false;
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribe();
}
protected hassSubscribe() { protected hassSubscribe() {
return [ return [
subscribeConditions(this.hass, (conditions) => subscribeLabFeatures(this.hass!.connection, (features) => {
this._addConditions(conditions) this._newTriggersAndConditions =
), features.find(
(feature) =>
feature.domain === "automation" &&
feature.preview_feature === "new_triggers_conditions"
)?.enabled ?? false;
}),
]; ];
} }
private _addConditions(conditions: ConditionDescriptions) { private _subscribeDescriptions() {
this._conditionDescriptions = { this._unsubscribe();
...this._conditionDescriptions, this._conditionDescriptions = {};
...conditions, this._unsub = subscribeConditions(this.hass, (descriptions) => {
}; this._conditionDescriptions = {
...this._conditionDescriptions,
...descriptions,
};
});
}
private _unsubscribe() {
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("_newTriggersAndConditions")) {
this._subscribeDescriptions();
}
} }
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {

View File

@@ -69,6 +69,45 @@ export class HaPlatformCondition extends LitElement {
} else { } else {
this._manifest = undefined; this._manifest = undefined;
} }
if (
oldValue?.condition !== this.condition?.condition &&
this.condition &&
this.description?.fields
) {
let updatedDefaultValue = false;
const updatedOptions = {};
const loadDefaults = !("options" in this.condition);
// Set mandatory bools without a default value to false
Object.entries(this.description.fields).forEach(([key, field]) => {
if (
field.selector &&
field.required &&
field.default === undefined &&
"boolean" in field.selector &&
updatedOptions[key] === undefined
) {
updatedDefaultValue = true;
updatedOptions[key] = false;
} else if (
loadDefaults &&
field.selector &&
field.default !== undefined &&
updatedOptions[key] === undefined
) {
updatedDefaultValue = true;
updatedOptions[key] = field.default;
}
});
if (updatedDefaultValue) {
fireEvent(this, "value-changed", {
value: {
...this.condition,
options: updatedOptions,
},
});
}
}
} }
protected render() { protected render() {
@@ -85,9 +124,9 @@ export class HaPlatformCondition extends LitElement {
const hasOptional = Boolean( const hasOptional = Boolean(
conditionDesc?.fields && conditionDesc?.fields &&
Object.values(conditionDesc.fields).some((field) => Object.values(conditionDesc.fields).some((field) =>
showOptionalToggle(field) showOptionalToggle(field)
) )
); );
return html` return html`
@@ -354,6 +393,10 @@ export class HaPlatformCondition extends LitElement {
} }
static styles = css` static styles = css`
:host {
display: block;
margin: 0px calc(-1 * var(--ha-space-4));
}
ha-settings-row { ha-settings-row {
padding: 0 var(--ha-space-4); padding: 0 var(--ha-space-4);
} }

View File

@@ -23,7 +23,7 @@ import {
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { transform } from "../../../common/decorators/transform"; import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
@@ -112,6 +112,7 @@ declare global {
} }
} }
@customElement("ha-automation-editor")
export class HaAutomationEditor extends PreventUnsavedMixin( export class HaAutomationEditor extends PreventUnsavedMixin(
KeyboardShortcutMixin(LitElement) KeyboardShortcutMixin(LitElement)
) { ) {
@@ -1339,5 +1340,3 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
]; ];
} }
} }
customElements.define("ha-automation-editor", HaAutomationEditor);

View File

@@ -35,6 +35,8 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import "../../../components/ha-tooltip";
import type { LocalizeFunc } from "../../../common/translations/localize"; import type { LocalizeFunc } from "../../../common/translations/localize";
import { import {
hasRejectedItems, hasRejectedItems,
@@ -327,14 +329,19 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
const date = new Date(automation.last_triggered); const date = new Date(automation.last_triggered);
const now = new Date(); const now = new Date();
const dayDifference = differenceInDays(now, date); const dayDifference = differenceInDays(now, date);
const formattedTime = formatShortDateTimeWithConditionalYear(
date,
this.hass.locale,
this.hass.config
);
const elementId = "last-triggered-" + slugify(automation.entity_id);
return html` return html`
${dayDifference > 3 ${dayDifference > 3
? formatShortDateTimeWithConditionalYear( ? formattedTime
date, : html`
this.hass.locale, <ha-tooltip for=${elementId}>${formattedTime}</ha-tooltip>
this.hass.config <span id=${elementId}>${relativeTime(date, locale)}</span>
) `}
: relativeTime(date, locale)}
`; `;
}, },
}, },

View File

@@ -72,7 +72,6 @@ import "./types/ha-automation-trigger-event";
import "./types/ha-automation-trigger-geo_location"; import "./types/ha-automation-trigger-geo_location";
import "./types/ha-automation-trigger-homeassistant"; import "./types/ha-automation-trigger-homeassistant";
import "./types/ha-automation-trigger-list"; import "./types/ha-automation-trigger-list";
import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state"; import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-persistent_notification"; import "./types/ha-automation-trigger-persistent_notification";
import "./types/ha-automation-trigger-platform"; import "./types/ha-automation-trigger-platform";

View File

@@ -1,6 +1,9 @@
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js"; import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import type { HassServiceTarget } from "home-assistant-js-websocket"; import type {
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@@ -21,6 +24,7 @@ import {
type Trigger, type Trigger,
type TriggerList, type TriggerList,
} from "../../../../data/automation"; } from "../../../../data/automation";
import { subscribeLabFeatures } from "../../../../data/labs";
import type { TriggerDescriptions } from "../../../../data/trigger"; import type { TriggerDescriptions } from "../../../../data/trigger";
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger"; import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
@@ -67,16 +71,54 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
private _triggerKeys = new WeakMap<Trigger, string>(); private _triggerKeys = new WeakMap<Trigger, string>();
private _unsub?: Promise<UnsubscribeFunc>;
@state() private _triggerDescriptions: TriggerDescriptions = {}; @state() private _triggerDescriptions: TriggerDescriptions = {};
// @ts-ignore
@state() private _newTriggersAndConditions = false;
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribe();
}
protected hassSubscribe() { protected hassSubscribe() {
return [ return [
subscribeTriggers(this.hass, (triggers) => this._addTriggers(triggers)), subscribeLabFeatures(this.hass!.connection, (features) => {
this._newTriggersAndConditions =
features.find(
(feature) =>
feature.domain === "automation" &&
feature.preview_feature === "new_triggers_conditions"
)?.enabled ?? false;
}),
]; ];
} }
private _addTriggers(triggers: TriggerDescriptions) { private _subscribeDescriptions() {
this._triggerDescriptions = { ...this._triggerDescriptions, ...triggers }; this._unsubscribe();
this._triggerDescriptions = {};
this._unsub = subscribeTriggers(this.hass, (descriptions) => {
this._triggerDescriptions = {
...this._triggerDescriptions,
...descriptions,
};
});
}
private _unsubscribe() {
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("_newTriggersAndConditions")) {
this._subscribeDescriptions();
}
} }
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {

View File

@@ -1,58 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { MqttTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import type { TriggerElement } from "../ha-automation-trigger-row";
const SCHEMA = [
{ name: "topic", required: true, selector: { text: {} } },
{ name: "payload", selector: { text: {} } },
] as const;
@customElement("ha-automation-trigger-mqtt")
export class HaMQTTTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trigger!: MqttTrigger;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): MqttTrigger {
return { trigger: "mqtt", topic: "" };
}
protected render() {
return html`
<ha-form
.schema=${SCHEMA}
.data=${this.trigger}
.hass=${this.hass}
.disabled=${this.disabled}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newTrigger = ev.detail.value;
fireEvent(this, "value-changed", { value: newTrigger });
}
private _computeLabelCallback = (
schema: SchemaUnion<typeof SCHEMA>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.mqtt.${schema.name}`
);
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-mqtt": HaMQTTTrigger;
}
}

View File

@@ -25,6 +25,16 @@ const showOptionalToggle = (field: TriggerDescription["fields"][string]) =>
!field.required && !field.required &&
!("boolean" in field.selector && field.default); !("boolean" in field.selector && field.default);
const DEFAULT_KEYS: (keyof PlatformTrigger)[] = [
"trigger",
"target",
"alias",
"id",
"variables",
"enabled",
"options",
] as const;
@customElement("ha-automation-trigger-platform") @customElement("ha-automation-trigger-platform")
export class HaPlatformTrigger extends LitElement { export class HaPlatformTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -52,6 +62,31 @@ export class HaPlatformTrigger extends LitElement {
if (!changedProperties.has("trigger")) { if (!changedProperties.has("trigger")) {
return; return;
} }
let newValue: PlatformTrigger | undefined;
for (const key in this.trigger) {
// Migrate old options to `options`
if (DEFAULT_KEYS.includes(key as keyof PlatformTrigger)) {
continue;
}
if (newValue === undefined) {
newValue = {
...this.trigger,
options: { [key]: this.trigger[key] },
};
} else {
newValue.options![key] = this.trigger[key];
}
delete newValue[key];
}
if (newValue !== undefined) {
fireEvent(this, "value-changed", {
value: newValue,
});
this.trigger = newValue;
}
const oldValue = changedProperties.get("trigger") as const oldValue = changedProperties.get("trigger") as
| undefined | undefined
| this["trigger"]; | this["trigger"];
@@ -69,6 +104,46 @@ export class HaPlatformTrigger extends LitElement {
} else { } else {
this._manifest = undefined; this._manifest = undefined;
} }
if (
oldValue?.trigger !== this.trigger?.trigger &&
this.trigger &&
this.description?.fields
) {
let updatedDefaultValue = false;
const updatedOptions = {};
const loadDefaults = !("options" in this.trigger);
// Set mandatory bools without a default value to false
Object.entries(this.description.fields).forEach(([key, field]) => {
if (
field.selector &&
field.required &&
field.default === undefined &&
"boolean" in field.selector &&
updatedOptions[key] === undefined
) {
updatedDefaultValue = true;
updatedOptions[key] = false;
} else if (
loadDefaults &&
field.selector &&
field.default !== undefined &&
updatedOptions[key] === undefined
) {
updatedDefaultValue = true;
updatedOptions[key] = field.default;
}
});
if (updatedDefaultValue) {
fireEvent(this, "value-changed", {
value: {
...this.trigger,
options: updatedOptions,
},
});
}
}
} }
protected render() { protected render() {
@@ -85,9 +160,9 @@ export class HaPlatformTrigger extends LitElement {
const hasOptional = Boolean( const hasOptional = Boolean(
triggerDesc?.fields && triggerDesc?.fields &&
Object.values(triggerDesc.fields).some((field) => Object.values(triggerDesc.fields).some((field) =>
showOptionalToggle(field) showOptionalToggle(field)
) )
); );
return html` return html`
@@ -354,6 +429,10 @@ export class HaPlatformTrigger extends LitElement {
} }
static styles = css` static styles = css`
:host {
display: block;
margin: 0px calc(-1 * var(--ha-space-4));
}
ha-settings-row { ha-settings-row {
padding: 0 var(--ha-space-4); padding: 0 var(--ha-space-4);
} }

View File

@@ -1,7 +1,7 @@
import { mdiOpenInNew } from "@mdi/js"; import { mdiOpenInNew } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { createCloseHeading } from "../../../../components/ha-dialog"; import { createCloseHeading } from "../../../../components/ha-dialog";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
@@ -13,6 +13,7 @@ import type { WebhookDialogParams } from "./show-dialog-manage-cloudhook";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-copy-textfield"; import "../../../../components/ha-copy-textfield";
@customElement("dialog-manage-cloudhook")
export class DialogManageCloudhook extends LitElement { export class DialogManageCloudhook extends LitElement {
protected hass?: HomeAssistant; protected hass?: HomeAssistant;
@@ -155,5 +156,3 @@ declare global {
"dialog-manage-cloudhook": DialogManageCloudhook; "dialog-manage-cloudhook": DialogManageCloudhook;
} }
} }
customElements.define("dialog-manage-cloudhook", DialogManageCloudhook);

View File

@@ -0,0 +1,52 @@
import { mdiKey } from "@mdi/js";
import { getConfigEntries } from "../../../../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchESPHomeEncryptionKey } from "../../../../../../data/esphome";
import type { HomeAssistant } from "../../../../../../types";
import { showESPHomeEncryptionKeyDialog } from "../../../../integrations/integration-panels/esphome/show-dialog-esphome-encryption-key";
import type { DeviceAction } from "../../../ha-config-device-page";
export const getESPHomeDeviceActions = async (
el: HTMLElement,
hass: HomeAssistant,
device: DeviceRegistryEntry
): Promise<DeviceAction[]> => {
const actions: DeviceAction[] = [];
const configEntries = await getConfigEntries(hass, {
domain: "esphome",
});
const configEntry = configEntries.find((entry) =>
device.config_entries.includes(entry.entry_id)
);
if (!configEntry) {
return [];
}
const entryId = configEntry.entry_id;
try {
const encryptionKey = await fetchESPHomeEncryptionKey(hass, entryId);
if (encryptionKey.encryption_key) {
actions.push({
label: hass.localize(
"ui.panel.config.devices.esphome.show_encryption_key"
),
icon: mdiKey,
action: () =>
showESPHomeEncryptionKeyDialog(el, {
entry_id: entryId,
encryption_key: encryptionKey.encryption_key,
}),
});
}
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to fetch ESPHome encryption key:", err);
}
return actions;
};

View File

@@ -1138,23 +1138,20 @@ export class HaConfigDevicePage extends LitElement {
} }
if (domains.includes("mqtt")) { if (domains.includes("mqtt")) {
const mqtt = await import( const mqtt =
"./device-detail/integration-elements/mqtt/device-actions" await import("./device-detail/integration-elements/mqtt/device-actions");
);
const actions = mqtt.getMQTTDeviceActions(this, device); const actions = mqtt.getMQTTDeviceActions(this, device);
deviceActions.push(...actions); deviceActions.push(...actions);
} }
if (domains.includes("zha")) { if (domains.includes("zha")) {
const zha = await import( const zha =
"./device-detail/integration-elements/zha/device-actions" await import("./device-detail/integration-elements/zha/device-actions");
);
const actions = await zha.getZHADeviceActions(this, this.hass, device); const actions = await zha.getZHADeviceActions(this, this.hass, device);
deviceActions.push(...actions); deviceActions.push(...actions);
} }
if (domains.includes("zwave_js")) { if (domains.includes("zwave_js")) {
const zwave = await import( const zwave =
"./device-detail/integration-elements/zwave_js/device-actions" await import("./device-detail/integration-elements/zwave_js/device-actions");
);
const actions = await zwave.getZwaveDeviceActions( const actions = await zwave.getZwaveDeviceActions(
this, this,
this.hass, this.hass,
@@ -1162,10 +1159,19 @@ export class HaConfigDevicePage extends LitElement {
); );
deviceActions.push(...actions); deviceActions.push(...actions);
} }
if (domains.includes("matter")) { if (domains.includes("esphome")) {
const matter = await import( const esphome =
"./device-detail/integration-elements/matter/device-actions" await import("./device-detail/integration-elements/esphome/device-actions");
const actions = await esphome.getESPHomeDeviceActions(
this,
this.hass,
device
); );
deviceActions.push(...actions);
}
if (domains.includes("matter")) {
const matter =
await import("./device-detail/integration-elements/matter/device-actions");
const defaultActions = matter.getMatterDeviceDefaultActions( const defaultActions = matter.getMatterDeviceDefaultActions(
this, this,
this.hass, this.hass,
@@ -1209,9 +1215,8 @@ export class HaConfigDevicePage extends LitElement {
).map((int) => int.domain); ).map((int) => int.domain);
if (domains.includes("zwave_js")) { if (domains.includes("zwave_js")) {
const zwave = await import( const zwave =
"./device-detail/integration-elements/zwave_js/device-alerts" await import("./device-detail/integration-elements/zwave_js/device-alerts");
);
const alerts = await zwave.getZwaveDeviceAlerts(this.hass, device); const alerts = await zwave.getZwaveDeviceAlerts(this.hass, device);
deviceAlerts.push(...alerts); deviceAlerts.push(...alerts);
@@ -1293,9 +1298,7 @@ export class HaConfigDevicePage extends LitElement {
`); `);
} }
if (domains.includes("zwave_js")) { if (domains.includes("zwave_js")) {
import( import("./device-detail/integration-elements/zwave_js/ha-device-info-zwave_js");
"./device-detail/integration-elements/zwave_js/ha-device-info-zwave_js"
);
deviceInfo.push(html` deviceInfo.push(html`
<ha-device-info-zwave_js <ha-device-info-zwave_js
.hass=${this.hass} .hass=${this.hass}
@@ -1304,9 +1307,7 @@ export class HaConfigDevicePage extends LitElement {
`); `);
} }
if (domains.includes("matter")) { if (domains.includes("matter")) {
import( import("./device-detail/integration-elements/matter/ha-device-info-matter");
"./device-detail/integration-elements/matter/ha-device-info-matter"
);
deviceInfo.push(html` deviceInfo.push(html`
<ha-device-info-matter <ha-device-info-matter
.hass=${this.hass} .hass=${this.hass}

View File

@@ -1012,7 +1012,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
? html`<ha-area-picker ? html`<ha-area-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this._areaId} .value=${this._areaId}
.placeholder=${this._device?.area_id}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
></ha-area-picker>` ></ha-area-picker>`

View File

@@ -116,8 +116,10 @@ import { showAddIntegrationDialog } from "../integrations/show-add-integration-d
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { slugify } from "../../../common/string/slugify"; import { slugify } from "../../../common/string/slugify";
export interface StateEntity export interface StateEntity extends Omit<
extends Omit<EntityRegistryEntry, "id" | "unique_id"> { EntityRegistryEntry,
"id" | "unique_id"
> {
readonly?: boolean; readonly?: boolean;
selectable?: boolean; selectable?: boolean;
id?: string; id?: string;

View File

@@ -530,9 +530,7 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
zha: { zha: {
tag: "zha-config-dashboard-router", tag: "zha-config-dashboard-router",
load: () => load: () =>
import( import("./integrations/integration-panels/zha/zha-config-dashboard-router"),
"./integrations/integration-panels/zha/zha-config-dashboard-router"
),
}, },
mqtt: { mqtt: {
tag: "mqtt-config-panel", tag: "mqtt-config-panel",
@@ -542,30 +540,22 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
zwave_js: { zwave_js: {
tag: "zwave_js-config-router", tag: "zwave_js-config-router",
load: () => load: () =>
import( import("./integrations/integration-panels/zwave_js/zwave_js-config-router"),
"./integrations/integration-panels/zwave_js/zwave_js-config-router"
),
}, },
matter: { matter: {
tag: "matter-config-panel", tag: "matter-config-panel",
load: () => load: () =>
import( import("./integrations/integration-panels/matter/matter-config-panel"),
"./integrations/integration-panels/matter/matter-config-panel"
),
}, },
thread: { thread: {
tag: "thread-config-panel", tag: "thread-config-panel",
load: () => load: () =>
import( import("./integrations/integration-panels/thread/thread-config-panel"),
"./integrations/integration-panels/thread/thread-config-panel"
),
}, },
bluetooth: { bluetooth: {
tag: "bluetooth-config-dashboard-router", tag: "bluetooth-config-dashboard-router",
load: () => load: () =>
import( import("./integrations/integration-panels/bluetooth/bluetooth-config-dashboard-router"),
"./integrations/integration-panels/bluetooth/bluetooth-config-dashboard-router"
),
}, },
dhcp: { dhcp: {
tag: "dhcp-config-panel", tag: "dhcp-config-panel",
@@ -580,9 +570,7 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
zeroconf: { zeroconf: {
tag: "zeroconf-config-panel", tag: "zeroconf-config-panel",
load: () => load: () =>
import( import("./integrations/integration-panels/zeroconf/zeroconf-config-panel"),
"./integrations/integration-panels/zeroconf/zeroconf-config-panel"
),
}, },
application_credentials: { application_credentials: {
tag: "ha-config-application-credentials", tag: "ha-config-application-credentials",

View File

@@ -1,7 +1,7 @@
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { createCloseHeading } from "../../../../components/ha-dialog"; import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
@@ -14,6 +14,7 @@ import type {
} from "./show-dialog-schedule-block-info"; } from "./show-dialog-schedule-block-info";
import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { SchemaUnion } from "../../../../components/ha-form/types";
@customElement("dialog-schedule-block-info")
class DialogScheduleBlockInfo extends LitElement { class DialogScheduleBlockInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -156,5 +157,3 @@ declare global {
"dialog-schedule-block-info": DialogScheduleBlockInfo; "dialog-schedule-block-info": DialogScheduleBlockInfo;
} }
} }
customElements.define("dialog-schedule-block-info", DialogScheduleBlockInfo);

View File

@@ -968,12 +968,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
}; };
} }
protected supportedSingleKeyShortcuts(): SupportedShortcuts {
return {
n: () => this._createFlow(),
};
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@@ -0,0 +1,140 @@
import { mdiClose, mdiContentCopy } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../../common/util/copy-clipboard";
import "../../../../../components/ha-button";
import "../../../../../components/ha-dialog-footer";
import "../../../../../components/ha-dialog-header";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-wa-dialog";
import { haStyleDialog } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { showToast } from "../../../../../util/toast";
import type { ESPHomeEncryptionKeyDialogParams } from "./show-dialog-esphome-encryption-key";
@customElement("dialog-esphome-encryption-key")
class DialogESPHomeEncryptionKey extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: ESPHomeEncryptionKeyDialogParams;
public async showDialog(
params: ESPHomeEncryptionKeyDialogParams
): Promise<void> {
this._params = params;
}
public closeDialog(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-wa-dialog
open
@closed=${this.closeDialog}
header-title=${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_title"
)}
>
<ha-dialog-header slot="heading">
<ha-icon-button
slot="navigationIcon"
dialogAction="cancel"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">
${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_title"
)}
</span>
</ha-dialog-header>
<div class="content">
<p>
${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_description"
)}
</p>
<div class="key-row">
<div class="key-container">
<code>${this._params.encryption_key}</code>
</div>
<ha-icon-button
@click=${this._copyToClipboard}
.label=${this.hass.localize("ui.common.copy")}
.path=${mdiContentCopy}
></ha-icon-button>
</div>
</div>
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" data-dialog="close">
${this.hass.localize("ui.common.close")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
private async _copyToClipboard(): Promise<void> {
if (!this._params?.encryption_key) {
return;
}
await copyToClipboard(this._params.encryption_key);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
.content {
display: flex;
flex-direction: column;
gap: var(--ha-space-6);
}
.key-row {
display: flex;
gap: var(--ha-space-2);
align-items: center;
}
.key-container {
flex: 1;
border-radius: var(--ha-space-2);
border: 1px solid var(--divider-color);
background-color: var(
--code-editor-background-color,
var(--secondary-background-color)
);
padding: var(--ha-space-3);
overflow: auto;
}
p {
margin: 0;
color: var(--secondary-text-color);
line-height: var(--ha-line-height-condensed);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-esphome-encryption-key": DialogESPHomeEncryptionKey;
}
}

View File

@@ -0,0 +1,20 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface ESPHomeEncryptionKeyDialogParams {
entry_id: string;
encryption_key: string;
}
export const loadESPHomeEncryptionKeyDialog = () =>
import("./dialog-esphome-encryption-key");
export const showESPHomeEncryptionKeyDialog = (
element: HTMLElement,
dialogParams: ESPHomeEncryptionKeyDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-esphome-encryption-key",
dialogImport: loadESPHomeEncryptionKeyDialog,
dialogParams,
});
};

View File

@@ -1,6 +1,6 @@
import type { CSSResultGroup, PropertyValues } from "lit"; import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
@@ -15,6 +15,7 @@ import type { HomeAssistant } from "../../../../../types";
import { formatAsPaddedHex } from "./functions"; import { formatAsPaddedHex } from "./functions";
import type { IssueCommandServiceData } from "./types"; import type { IssueCommandServiceData } from "./types";
@customElement("zha-cluster-commands")
export class ZHAClusterCommands extends LitElement { export class ZHAClusterCommands extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@@ -259,5 +260,3 @@ declare global {
"zha-cluster-commands": ZHAClusterCommands; "zha-cluster-commands": ZHAClusterCommands;
} }
} }
customElements.define("zha-cluster-commands", ZHAClusterCommands);

View File

@@ -192,9 +192,9 @@ export class ZHAGroupBindingControl extends LitElement {
private get _canBind(): boolean { private get _canBind(): boolean {
return Boolean( return Boolean(
this._groupToBind && this._groupToBind &&
this._clustersToBind && this._clustersToBind &&
this._clustersToBind?.length > 0 && this._clustersToBind?.length > 0 &&
this.device this.device
); );
} }

View File

@@ -55,7 +55,6 @@ import {
subscribeEntityRegistry, subscribeEntityRegistry,
updateEntityRegistryEntry, updateEntityRegistryEntry,
} from "../../../../../../data/entity_registry"; } from "../../../../../../data/entity_registry";
import { SubscribeMixin } from "../../../../../../mixins/subscribe-mixin";
import "./zwave-js-add-node-added-insecure"; import "./zwave-js-add-node-added-insecure";
import "./zwave-js-add-node-code-input"; import "./zwave-js-add-node-code-input";
import "./zwave-js-add-node-configure-device"; import "./zwave-js-add-node-configure-device";
@@ -69,7 +68,7 @@ import "./zwave-js-add-node-select-security-strategy";
const INCLUSION_TIMEOUT_MINUTES = 5; const INCLUSION_TIMEOUT_MINUTES = 5;
@customElement("dialog-zwave_js-add-node") @customElement("dialog-zwave_js-add-node")
class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) { class DialogZWaveJSAddNode extends LitElement {
// #region variables // #region variables
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -103,6 +102,8 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
@state() private _securityClasses: SecurityClass[] = []; @state() private _securityClasses: SecurityClass[] = [];
@state() private _entities: EntityRegistryEntry[] = [];
@state() private _codeInput = ""; @state() private _codeInput = "";
@query("ha-dialog") private _dialog?: HaDialog; @query("ha-dialog") private _dialog?: HaDialog;
@@ -113,22 +114,14 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
private _onStop?: () => void; private _onStop?: () => void;
private _subscribed?: Promise<UnsubscribeFunc | undefined>; private _subscribedAddZwaveNode?: Promise<UnsubscribeFunc | undefined>;
private _newDeviceSubscription?: Promise<UnsubscribeFunc | undefined>; private _newDeviceSubscription?: Promise<UnsubscribeFunc | undefined>;
@state() private _entities: EntityRegistryEntry[] = []; private _subscribedEntityRegistry?: UnsubscribeFunc;
// #endregion // #endregion
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection, (entities) => {
this._entities = entities;
}),
];
}
protected render() { protected render() {
if (!this._entryId) { if (!this._entryId) {
return nothing; return nothing;
@@ -439,11 +432,6 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
></zwave-js-add-node-loading>`; ></zwave-js-add-node-loading>`;
} }
public connectedCallback(): void {
super.connectedCallback();
window.addEventListener("beforeunload", this._onBeforeUnload);
}
private _onBeforeUnload = (event: BeforeUnloadEvent) => { private _onBeforeUnload = (event: BeforeUnloadEvent) => {
if (this._step && this._shouldPreventClose(this._step)) { if (this._step && this._shouldPreventClose(this._step)) {
event.preventDefault(); event.preventDefault();
@@ -468,6 +456,14 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
} }
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> { public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> {
window.addEventListener("beforeunload", this._onBeforeUnload);
this._subscribedEntityRegistry = subscribeEntityRegistry(
this.hass.connection,
(entities) => {
this._entities = entities;
}
);
if (this._step) { if (this._step) {
// already started // already started
return; return;
@@ -562,7 +558,7 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
this._step = "select_method"; this._step = "select_method";
break; break;
case "search_devices": case "search_devices":
this._unsubscribe(); this._unsubscribeAddZwaveNode();
if ( if (
this._supportsSmartStart && this._supportsSmartStart &&
this.hass.auth.external?.config.hasBarCodeScanner this.hass.auth.external?.config.hasBarCodeScanner
@@ -604,7 +600,7 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
} }
private _searchDevicesShowSecurityOptions() { private _searchDevicesShowSecurityOptions() {
this._unsubscribe(); this._unsubscribeAddZwaveNode();
this._step = "choose_security_strategy"; this._step = "choose_security_strategy";
} }
@@ -626,7 +622,7 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
this._lowSecurity = false; this._lowSecurity = false;
const s2Device = qrProvisioningInformation || dsk; const s2Device = qrProvisioningInformation || dsk;
this._subscribed = subscribeAddZwaveNode( this._subscribedAddZwaveNode = subscribeAddZwaveNode(
this.hass, this.hass,
this._entryId!, this._entryId!,
(message) => { (message) => {
@@ -635,7 +631,7 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
this._step = s2Device ? "search_s2_device" : "search_devices"; this._step = s2Device ? "search_s2_device" : "search_devices";
break; break;
case "inclusion failed": case "inclusion failed":
this._unsubscribe(); this._unsubscribeAddZwaveNode();
this._step = "failed"; this._step = "failed";
break; break;
case "inclusion stopped": case "inclusion stopped":
@@ -677,7 +673,7 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
this._lowSecurityReason = message.node.low_security_reason; this._lowSecurityReason = message.node.low_security_reason;
break; break;
case "interview completed": case "interview completed":
this._unsubscribe(); this._unsubscribeAddZwaveNode();
this._step = "configure_device"; this._step = "configure_device";
break; break;
} }
@@ -694,7 +690,7 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
}); });
this._addNodeTimeoutHandle = window.setTimeout( this._addNodeTimeoutHandle = window.setTimeout(
() => { () => {
this._unsubscribe(); this._unsubscribeAddZwaveNode();
this._error = this.hass.localize( this._error = this.hass.localize(
"ui.panel.config.zwave_js.add_node.timeout_error", "ui.panel.config.zwave_js.add_node.timeout_error",
{ minutes: INCLUSION_TIMEOUT_MINUTES } { minutes: INCLUSION_TIMEOUT_MINUTES }
@@ -1023,10 +1019,10 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
} }
} }
private _unsubscribe(): void { private _unsubscribeAddZwaveNode(): void {
if (this._subscribed) { if (this._subscribedAddZwaveNode) {
this._subscribed.then((unsub) => unsub && unsub()); this._subscribedAddZwaveNode.then((unsub) => unsub && unsub());
this._subscribed = undefined; this._subscribedAddZwaveNode = undefined;
if (this._entryId) { if (this._entryId) {
stopZwaveInclusion(this.hass, this._entryId); stopZwaveInclusion(this.hass, this._entryId);
@@ -1060,8 +1056,17 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
window.removeEventListener("beforeunload", this._onBeforeUnload); window.removeEventListener("beforeunload", this._onBeforeUnload);
} }
private _unsubscribeDialog() {
if (this._subscribedEntityRegistry) {
this._subscribedEntityRegistry();
this._subscribedEntityRegistry = undefined;
}
}
private _dialogClosed() { private _dialogClosed() {
this._unsubscribe(); window.removeEventListener("beforeunload", this._onBeforeUnload);
this._unsubscribeAddZwaveNode();
this._unsubscribeDialog();
this._open = false; this._open = false;
this._entryId = undefined; this._entryId = undefined;
this._step = undefined; this._step = undefined;
@@ -1100,7 +1105,8 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
super.disconnectedCallback(); super.disconnectedCallback();
window.removeEventListener("beforeunload", this._onBeforeUnload); window.removeEventListener("beforeunload", this._onBeforeUnload);
this._unsubscribe(); this._unsubscribeAddZwaveNode();
this._unsubscribeDialog();
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -1,5 +1,6 @@
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { relativeTime } from "../../../common/datetime/relative_time"; import { relativeTime } from "../../../common/datetime/relative_time";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button"; import "../../../components/ha-button";
@@ -11,6 +12,7 @@ import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import type { BackupConfig } from "../../../data/backup"; import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup"; import { fetchBackupConfig } from "../../../data/backup";
import { getSupervisorUpdateConfig } from "../../../data/supervisor/update";
import type { HassDialog } from "../../../dialogs/make-dialog-manager"; import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LabsPreviewFeatureEnableDialogParams } from "./show-dialog-labs-preview-feature-enable"; import type { LabsPreviewFeatureEnableDialogParams } from "./show-dialog-labs-preview-feature-enable";
@@ -35,7 +37,10 @@ export class DialogLabsPreviewFeatureEnable
): Promise<void> { ): Promise<void> {
this._params = params; this._params = params;
this._createBackup = false; this._createBackup = false;
await this._fetchBackupConfig(); this._fetchBackupConfig();
if (isComponentLoaded(this.hass, "hassio")) {
this._fetchUpdateBackupConfig();
}
} }
public closeDialog(): boolean { public closeDialog(): boolean {
@@ -54,15 +59,21 @@ export class DialogLabsPreviewFeatureEnable
try { try {
const { config } = await fetchBackupConfig(this.hass); const { config } = await fetchBackupConfig(this.hass);
this._backupConfig = config; this._backupConfig = config;
} catch (err) {
// Ignore error, user will get manual backup option
// eslint-disable-next-line no-console
console.error(err);
}
}
// Default to enabled if automatic backups are configured, disabled otherwise private async _fetchUpdateBackupConfig() {
this._createBackup = try {
config.automatic_backups_configured && const config = await getSupervisorUpdateConfig(this.hass);
!!config.create_backup.password && this._createBackup = config.core_backup_before_update;
config.create_backup.agent_ids.length > 0; } catch (err) {
} catch { // Ignore error, user can still toggle the switch manually
// User will get manual backup option if fetch fails // eslint-disable-next-line no-console
this._createBackup = false; console.error(err);
} }
} }

View File

@@ -94,7 +94,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
<hass-subpage <hass-subpage
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
back-path="/config" back-path="/config/system"
.header=${this.hass.localize("ui.panel.config.labs.caption")} .header=${this.hass.localize("ui.panel.config.labs.caption")}
> >
${sortedFeatures.length ${sortedFeatures.length
@@ -385,6 +385,10 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
display: block; display: block;
} }
a[slot="toolbar-icon"] {
color: var(--sidebar-icon-color);
}
.content { .content {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;

View File

@@ -1,7 +1,7 @@
import { mdiClose, mdiContentCopy } from "@mdi/js"; import { mdiClose, mdiContentCopy } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { copyToClipboard } from "../../../common/util/copy-clipboard"; import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
@@ -26,6 +26,7 @@ import { showToast } from "../../../util/toast";
import type { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail"; import type { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail";
import { formatSystemLogTime } from "./util"; import { formatSystemLogTime } from "./util";
@customElement("dialog-system-log-detail")
class DialogSystemLogDetail extends LitElement { class DialogSystemLogDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -272,5 +273,3 @@ declare global {
"dialog-system-log-detail": DialogSystemLogDetail; "dialog-system-log-detail": DialogSystemLogDetail;
} }
} }
customElements.define("dialog-system-log-detail", DialogSystemLogDetail);

View File

@@ -326,7 +326,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
PANEL_DASHBOARDS.forEach((panel) => { PANEL_DASHBOARDS.forEach((panel) => {
const panelInfo = this.hass.panels[panel]; const panelInfo = this.hass.panels[panel];
if (!panel) { if (!panelInfo) {
return; return;
} }
const item: DataTableItem = { const item: DataTableItem = {

View File

@@ -1,7 +1,7 @@
import { mdiPencil } from "@mdi/js"; import { mdiPencil } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entities-picker"; import "../../../components/entity/ha-entities-picker";
@@ -43,6 +43,7 @@ const cropOptions: CropOptions = {
aspectRatio: 1, aspectRatio: 1,
}; };
@customElement("dialog-person-detail")
class DialogPersonDetail extends LitElement implements HassDialog { class DialogPersonDetail extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -567,5 +568,3 @@ declare global {
"dialog-person-detail": DialogPersonDetail; "dialog-person-detail": DialogPersonDetail;
} }
} }
customElements.define("dialog-person-detail", DialogPersonDetail);

View File

@@ -24,7 +24,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color"; import { computeCssColor } from "../../../common/color/compute-color";
import { formatShortDateTime } from "../../../common/datetime/format_date_time"; import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time"; import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage"; import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../common/dom/fire_event";
@@ -301,10 +301,21 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
const date = new Date(scene.state); const date = new Date(scene.state);
const now = new Date(); const now = new Date();
const dayDifference = differenceInDays(now, date); const dayDifference = differenceInDays(now, date);
const formattedTime = formatShortDateTimeWithConditionalYear(
date,
this.hass.locale,
this.hass.config
);
const elementId = "last-activated-" + slugify(scene.entity_id);
return html` return html`
${dayDifference > 3 ${dayDifference > 3
? formatShortDateTime(date, this.hass.locale, this.hass.config) ? formattedTime
: relativeTime(date, this.hass.locale)} : html`
<ha-tooltip for=${elementId}>${formattedTime}</ha-tooltip>
<span id=${elementId}
>${relativeTime(date, this.hass.locale)}</span
>
`}
`; `;
}, },
}, },

View File

@@ -21,7 +21,7 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { goBack, navigate } from "../../../common/navigate"; import { goBack, navigate } from "../../../common/navigate";
@@ -79,6 +79,7 @@ import "./manual-script-editor";
import type { HaManualScriptEditor } from "./manual-script-editor"; import type { HaManualScriptEditor } from "./manual-script-editor";
import { showAutomationSaveTimeoutDialog } from "../automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout"; import { showAutomationSaveTimeoutDialog } from "../automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout";
@customElement("ha-script-editor")
export class HaScriptEditor extends SubscribeMixin( export class HaScriptEditor extends SubscribeMixin(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement)) PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
) { ) {
@@ -1278,8 +1279,6 @@ export class HaScriptEditor extends SubscribeMixin(
} }
} }
customElements.define("ha-script-editor", HaScriptEditor);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-script-editor": HaScriptEditor; "ha-script-editor": HaScriptEditor;

View File

@@ -33,6 +33,8 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import "../../../components/ha-tooltip";
import type { LocalizeFunc } from "../../../common/translations/localize"; import type { LocalizeFunc } from "../../../common/translations/localize";
import { import {
hasRejectedItems, hasRejectedItems,
@@ -302,19 +304,27 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
sortable: true, sortable: true,
title: localize("ui.card.automation.last_triggered"), title: localize("ui.card.automation.last_triggered"),
template: (script) => { template: (script) => {
if (!script.last_triggered) {
return this.hass.localize("ui.components.relative_time.never");
}
const date = new Date(script.last_triggered); const date = new Date(script.last_triggered);
const now = new Date(); const now = new Date();
const dayDifference = differenceInDays(now, date); const dayDifference = differenceInDays(now, date);
const formattedTime = formatShortDateTimeWithConditionalYear(
date,
this.hass.locale,
this.hass.config
);
const elementId = "last-triggered-" + slugify(script.entity_id);
return html` return html`
${script.last_triggered ${dayDifference > 3
? dayDifference > 3 ? formattedTime
? formatShortDateTimeWithConditionalYear( : html`
date, <ha-tooltip for=${elementId}>${formattedTime}</ha-tooltip>
this.hass.locale, <span id=${elementId}
this.hass.config >${relativeTime(date, this.hass.locale)}</span
) >
: relativeTime(date, this.hass.locale) `}
: this.hass.localize("ui.components.relative_time.never")}
`; `;
}, },
}, },

View File

@@ -1,6 +1,6 @@
import { mdiHelpCircle } from "@mdi/js"; import { mdiHelpCircle } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { isEmptyEntityDomainFilter } from "../../../common/entity/entity_domain_filter"; import { isEmptyEntityDomainFilter } from "../../../common/entity/entity_domain_filter";
@@ -20,6 +20,7 @@ import {
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
@customElement("cloud-alexa-pref")
export class CloudAlexaPref extends LitElement { export class CloudAlexaPref extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -297,5 +298,3 @@ declare global {
"cloud-alexa-pref": CloudAlexaPref; "cloud-alexa-pref": CloudAlexaPref;
} }
} }
customElements.define("cloud-alexa-pref", CloudAlexaPref);

View File

@@ -1,6 +1,6 @@
import { mdiHelpCircle } from "@mdi/js"; import { mdiHelpCircle } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { isEmptyEntityDomainFilter } from "../../../common/entity/entity_domain_filter"; import { isEmptyEntityDomainFilter } from "../../../common/entity/entity_domain_filter";
@@ -23,6 +23,7 @@ import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
import { showSaveSuccessToast } from "../../../util/toast-saved-success"; import { showSaveSuccessToast } from "../../../util/toast-saved-success";
@customElement("cloud-google-pref")
export class CloudGooglePref extends LitElement { export class CloudGooglePref extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -367,5 +368,3 @@ declare global {
"cloud-google-pref": CloudGooglePref; "cloud-google-pref": CloudGooglePref;
} }
} }
customElements.define("cloud-google-pref", CloudGooglePref);

View File

@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
@@ -19,6 +19,7 @@ const SCHEMA = [
}, },
]; ];
@customElement("dialog-home-zone-detail")
class DialogHomeZoneDetail extends LitElement { class DialogHomeZoneDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -151,5 +152,3 @@ declare global {
"dialog-home-zone-detail": DialogHomeZoneDetail; "dialog-home-zone-detail": DialogHomeZoneDetail;
} }
} }
customElements.define("dialog-home-zone-detail", DialogHomeZoneDetail);

View File

@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { addDistanceToCoord } from "../../../common/location/add_distance_to_coord"; import { addDistanceToCoord } from "../../../common/location/add_distance_to_coord";
@@ -14,6 +14,7 @@ import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { ZoneDetailDialogParams } from "./show-dialog-zone-detail"; import type { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
@customElement("dialog-zone-detail")
class DialogZoneDetail extends LitElement { class DialogZoneDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -241,5 +242,3 @@ declare global {
"dialog-zone-detail": DialogZoneDetail; "dialog-zone-detail": DialogZoneDetail;
} }
} }
customElements.define("dialog-zone-detail", DialogZoneDetail);

View File

@@ -47,12 +47,9 @@ import { configSections } from "../ha-panel-config";
import { showHomeZoneDetailDialog } from "./show-dialog-home-zone-detail"; import { showHomeZoneDetailDialog } from "./show-dialog-home-zone-detail";
import { showZoneDetailDialog } from "./show-dialog-zone-detail"; import { showZoneDetailDialog } from "./show-dialog-zone-detail";
import { slugify } from "../../../common/string/slugify"; import { slugify } from "../../../common/string/slugify";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
@customElement("ha-config-zone") @customElement("ha-config-zone")
export class HaConfigZone extends KeyboardShortcutMixin( export class HaConfigZone extends SubscribeMixin(LitElement) {
SubscribeMixin(LitElement)
) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false; @property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@@ -540,12 +537,6 @@ export class HaConfigZone extends KeyboardShortcutMixin(
}); });
} }
protected supportedSingleKeyShortcuts(): SupportedShortcuts {
return {
n: () => this._createZone(),
};
}
static styles = css` static styles = css`
hass-loading-screen { hass-loading-screen {
--app-header-background-color: var(--sidebar-background-color); --app-header-background-color: var(--sidebar-background-color);

View File

@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { ReactiveElement } from "lit"; import { ReactiveElement } from "lit";
import { property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { NavigateOptions } from "../../common/navigate"; import type { NavigateOptions } from "../../common/navigate";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
import { deepEqual } from "../../common/util/deep-equal"; import { deepEqual } from "../../common/util/deep-equal";
@@ -22,6 +22,7 @@ declare global {
} }
} }
@customElement("ha-panel-custom")
export class HaPanelCustom extends ReactiveElement { export class HaPanelCustom extends ReactiveElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -171,5 +172,3 @@ export class HaPanelCustom extends ReactiveElement {
iframeDoc.close(); iframeDoc.close();
} }
} }
customElements.define("ha-panel-custom", HaPanelCustom);

View File

@@ -1,7 +1,7 @@
import { mdiHelpCircle } from "@mdi/js"; import { mdiHelpCircle } from "@mdi/js";
import type { HassService } from "home-assistant-js-websocket"; import type { HassService } from "home-assistant-js-websocket";
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket"; import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
import { load } from "js-yaml"; import { dump, load } from "js-yaml";
import type { CSSResultGroup, TemplateResult } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@@ -137,7 +137,7 @@ class HaPanelDevAction extends LitElement {
const descriptionPlaceholders = const descriptionPlaceholders =
domain && serviceName domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders ? this.hass.services[domain]?.[serviceName]?.description_placeholders
: undefined; : undefined;
return html` return html`
@@ -320,7 +320,10 @@ class HaPanelDevAction extends LitElement {
${this.hass.localize( ${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${field.key}.example`, `component.${domain}.services.${serviceName}.fields.${field.key}.example`,
descriptionPlaceholders descriptionPlaceholders
) || field.example} ) ||
(typeof field.example === "object"
? html`<pre>${dump(field.example)}</pre>`
: field.example)}
</td> </td>
</tr>` </tr>`
)} )}

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