Compare commits

...
Author SHA1 Message Date
Wendelin 6b010b7ada Migrate getTriggerInfos to hassless 2026-05-26 09:12:17 +02:00
Jan-Philipp BeneckeandGitHub a147fc4fee Fix padding of vertical tile card content (#52198) 2026-05-26 08:21:05 +03:00
karwostsandGitHub a300085208 Fix calendar panel for non-admin (#52203) 2026-05-26 08:20:26 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
44989a6972 Update dependency date-fns to v4.3.0 (#52205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-26 08:19:30 +03:00
Yosi LevyandGitHub 54a8e6c294 RTL fix for automation row (#52200) 2026-05-25 12:38:34 +02:00
Yosi LevyandGitHub bfec22d828 RTL fix for new suggestion tree (#52199) 2026-05-25 11:52:54 +02:00
cde6450cfc Fix stale wake word display after wake word change in voice satellite set up wizard (#52194)
Fix stale wake word display after wake word change in satellite wizard

The config re-fetch was fire-and-forgotten, so the step transition to
STEP.WAKEWORD raced ahead with stale assistConfiguration. Awaiting the
fetch ensures the fresh active_wake_words are in place before rendering.

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
2026-05-25 07:41:56 +00:00
Petar PetrovandGitHub ab39e70629 Recover brand icons after Home Assistant restart (#52158)
* Recover brand icons after Home Assistant restart

* Make _refreshBrandsAccessToken async
2026-05-25 09:36:50 +02:00
J. Nick KostonandGitHub 69f209e3c3 Teach Bluetooth UI about auto scanning mode (#52192)
* Teach Bluetooth UI about auto scanning mode

* Drop unreachable auto cases and add isScannerStateMismatch tests
2026-05-25 09:30:03 +02:00
J. Nick KostonandGitHub f4c5561a54 Show raw advertisement bytes in Bluetooth device info (#52193)
* Show raw advertisement bytes in Bluetooth device info

* Use plain div for raw hex to avoid fragile pre whitespace
2026-05-25 09:24:46 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5147937a6f Update dependency generate-license-file to v4.2.1 (#52195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 09:24:42 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
ee39605aa7 Update dependency intl-messageformat to v11.2.7 (#52197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 09:24:38 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4af4f1dc51 Update dependency idb-keyval to v6.2.4 (#52190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-24 14:01:55 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a2d8859d94 Update dependency @date-fns/tz to v1.5.0 (#52187)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-24 14:03:30 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
afea8180c4 Update dependency idb-keyval to v6.2.3 (#52186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-24 13:53:50 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b9c077489d Bump github/codeql-action from 4.35.4 to 4.35.5 (#52183)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.4 to 4.35.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-24 09:33:38 +03:00
karwostsandGitHub 440bb32056 Remove unintended sort from select selector (#52179) 2026-05-23 21:14:24 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8f371621ad Update dependency @rspack/core to v2.0.4 (#52178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-23 16:18:20 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
61815b20e3 Update vitest monorepo to v4.1.7 (#52173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-23 13:03:54 +02:00
ildar170975andGitHub 1942fa3a77 hui-entity-editor: fix vertical spacings (#52170)
fix spacings
2026-05-23 10:04:46 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
865e67a06f Update Yarn to v4.15.0 (#52169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-23 10:03:23 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
412dce4c1f Update dependency tinykeys to v4 (#52172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-23 10:02:58 +02:00
Jan-Philipp BeneckeandGitHub ced2ac7ad5 Fix ha-drawer z-index (#52167)
Fix ha-drawer index
2026-05-22 20:33:05 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
6649f52bcd Update dependency tinykeys to v3.1.0 (#52166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-22 17:38:26 +00:00
7dbd6ae5a2 Add-on iframe: delegate microphone + camera Permissions Policy (#52068)
* Add-on iframe: delegate microphone + camera Permissions Policy

The add-on ingress iframe in ``ha-panel-app.ts`` ships without an
``allow=`` attribute, so the Permissions Policy default of *deny*
applies for ``microphone`` and ``camera`` on the cross-origin
iframe. An add-on that wants to call ``getUserMedia`` — voice
notes, dictation, video calls, photo capture — fails silently with
``NotAllowedError`` before the browser even surfaces the permission
prompt.

The failure is most visible on the Android Companion app, where
there's no "open in a new tab" escape: the user presses the mic
button and nothing happens, no toast, no logs.

Delegate ``microphone``, ``camera``, and ``clipboard-write`` to the
add-on iframe. Add-ons are first-party software the user explicitly
installs, and Chrome's runtime permission prompt still gates the
hardware access — the ``allow=`` attribute just lets the iframe
*request* the prompt instead of being blocked at the policy layer.

``clipboard-write`` is bundled in because the next-most-frequent
silent-fail in add-on land is ``navigator.clipboard.writeText`` for
"copy link" / "copy code" affordances, blocked by the same
mechanism.

* Sandbox add-on ingress iframe without allow-same-origin

Split IFRAME_SANDBOX into two constants: IFRAME_SANDBOX (without
allow-same-origin) for add-on ingress iframes that need origin
isolation, and IFRAME_SANDBOX_SAME_ORIGIN for external iframes
that need same-origin access.

This ensures add-on iframes can't inherit camera/microphone
permissions already granted to the Home Assistant origin, and
prevents same-origin iframes from removing their own sandbox.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Paulus Schoutsen <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-05-22 19:31:56 +02:00
WendelinandGitHub e1528d21b3 Migrate md-lists cloud dashboard and devtools (#52163)
Migrate lists in cloud and dev tools=
2026-05-22 18:24:16 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
79cb3137f2 Update tsparticles to v4.0.5 (#52162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-22 18:17:04 +03:00
Jan-Philipp BeneckeandGitHub 313360701a Revamp ZHA group page UI (#52124) 2026-05-22 16:24:59 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
b100d9577d Update formatjs monorepo (#52159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-22 15:56:02 +03:00
WendelinandGitHub 44ce303302 Fix dropdown keyboard scroll (#52157) 2026-05-22 13:47:43 +01:00
Aidan TimsonandGitHub 8f76613068 Add more quick links to device page (#52137)
* Add more quick links to device page

* Move shared keys to common location
2026-05-22 12:45:37 +03:00
85dff6640a Use ha-list-nav for each section in device page related card (#52142)
Co-authored-by: Wendelin <[email protected]>
2026-05-22 11:22:36 +02:00
Aidan TimsonandGitHub ab7c892b6b Add to for area page (#52141)
* Setup add to area page

* Remove 3 buttons, move to single add to button next to add a picture button

* Use normal size buttons

* Restructure layout with picture

* Remove div when both conditions are met

* Use mixin

* Fix imports
2026-05-22 12:02:38 +03:00
WendelinandGitHub 3fe57ad724 webawesome 3.7.0 (#52155) 2026-05-22 11:24:16 +03:00
1caf1d99b5 Migrate theme-picker to ha-generic-picker (#52067)
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-05-22 06:32:39 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
483df2fa2f Update dependency marked to v18.0.4 (#52153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-22 08:52:04 +03:00
e0adb006b6 Show time zone picker in onboarding when browser can't resolve IANA zone (#52146)
Some environments (e.g. Android WebView/emulator) return a UTC offset like
"+00:00" from Intl.DateTimeFormat().resolvedOptions().timeZone instead of an
IANA zone name. Submitting that to saveCoreConfig fails with "invalid time
zone", leaving users stuck on the country step.

Detect this by checking the resolved value against the google-timezones-json
list used by ha-timezone-picker, and surface the picker on the core-config
step when no IANA zone could be detected from the browser or the location
detect API.

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-22 08:26:50 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
50e34015b3 Update tsparticles to v4.0.4 (#52152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-22 08:23:24 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c1c926c631 Update tsparticles to v4.0.3 (#52148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-22 08:11:18 +03:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c41afac57c Update dependency typescript-eslint to v8.59.4 (#52147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-22 08:10:58 +03:00
Aidan TimsonandGitHub 8856c26929 Add quick links to area page, add area query param support (#52133)
* Add quick links to area page

* Typing

* Add query param support for areas

* hint for the rest

* Add support for helpers

* Add counts
2026-05-21 18:59:20 +02:00
WendelinandGitHub 4a0fe3190c Backups: Migrate md-list to new list-base (#52136)
Migrate md-list to new list-base
2026-05-21 18:57:24 +02:00
karwostsandGitHub 08f7e97462 Use display precision in statistic card (#52138) 2026-05-21 18:53:04 +02:00
Joakim PlateandGitHub a5791c8c08 Switch to power-standby for media player (#52127) 2026-05-21 18:53:01 +02:00
WendelinandGitHub 6a98a74c58 Fix trigger time margin bottom (#52144)
Fix trigger time margin
2026-05-21 18:39:35 +02:00
renovate[bot]andGitHub c1df3bc38e Update Node.js to v24.16.0 (#52140) 2026-05-21 17:03:13 +02:00
106 changed files with 4232 additions and 2390 deletions
+3 -3
View File
@@ -41,14 +41,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
languages: ${{ matrix.language }}
# 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)
- name: Autobuild
uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -62,4 +62,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
+1 -1
View File
@@ -1 +1 @@
24.15.0
24.16.0
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -13,4 +13,4 @@ nodeLinker: node-modules
npmMinimalAgeGate: 3d
yarnPath: .yarn/releases/yarn-4.14.1.cjs
yarnPath: .yarn/releases/yarn-4.15.0.cjs
@@ -75,7 +75,17 @@ export class DemoAutomationDescribeCondition extends LitElement {
<div class="condition">
<span>
${this._condition
? describeCondition(this._condition, this.hass, [])
? describeCondition(
this._condition,
this.hass.localize,
this.hass.locale,
[],
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue
)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -88,7 +98,19 @@ export class DemoAutomationDescribeCondition extends LitElement {
${conditions.map(
(conf) => html`
<div class="condition">
<span>${describeCondition(conf as any, this.hass, [])}</span>
<span
>${describeCondition(
conf as any,
this.hass.localize,
this.hass.locale,
[],
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue
)}</span
>
<pre>${dump(conf)}</pre>
</div>
`
@@ -99,7 +99,17 @@ export class DemoAutomationDescribeTrigger extends LitElement {
<div class="trigger">
<span>
${this._trigger
? describeTrigger(this._trigger, this.hass, [])
? describeTrigger(
this._trigger,
this.hass.localize,
this.hass.locale,
[],
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue
)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -111,7 +121,19 @@ export class DemoAutomationDescribeTrigger extends LitElement {
${triggers.map(
(conf) => html`
<div class="trigger">
<span>${describeTrigger(conf as any, this.hass, [])}</span>
<span
>${describeTrigger(
conf as any,
this.hass.localize,
this.hass.locale,
[],
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue
)}</span
>
<pre>${dump(conf)}</pre>
</div>
`
+25 -25
View File
@@ -38,24 +38,24 @@
"@codemirror/search": "6.7.0",
"@codemirror/state": "6.6.0",
"@codemirror/view": "6.43.0",
"@date-fns/tz": "1.4.1",
"@date-fns/tz": "1.5.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.4.5",
"@formatjs/intl-displaynames": "7.3.7",
"@formatjs/intl-durationformat": "0.10.11",
"@formatjs/intl-getcanonicallocales": "3.2.8",
"@formatjs/intl-listformat": "8.3.7",
"@formatjs/intl-locale": "5.3.7",
"@formatjs/intl-numberformat": "9.3.8",
"@formatjs/intl-pluralrules": "6.3.7",
"@formatjs/intl-relativetimeformat": "12.3.7",
"@formatjs/intl-datetimeformat": "7.4.6",
"@formatjs/intl-displaynames": "7.3.8",
"@formatjs/intl-durationformat": "0.10.12",
"@formatjs/intl-getcanonicallocales": "3.2.9",
"@formatjs/intl-listformat": "8.3.8",
"@formatjs/intl-locale": "5.3.8",
"@formatjs/intl-numberformat": "9.3.9",
"@formatjs/intl-pluralrules": "6.3.8",
"@formatjs/intl-relativetimeformat": "12.3.8",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
"@fullcalendar/list": "6.1.20",
"@fullcalendar/luxon3": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@home-assistant/webawesome": "3.3.1-ha.3",
"@home-assistant/webawesome": "3.7.0-ha.0",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.1.0",
"@lit-labs/observers": "2.1.0",
@@ -74,8 +74,8 @@
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.21",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "4.0.2",
"@tsparticles/preset-links": "4.0.2",
"@tsparticles/engine": "4.0.5",
"@tsparticles/preset-links": "4.0.5",
"@vibrant/color": "4.0.4",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
@@ -86,7 +86,7 @@
"core-js": "3.49.0",
"cropperjs": "1.6.2",
"culori": "4.0.2",
"date-fns": "4.2.1",
"date-fns": "4.3.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
@@ -97,8 +97,8 @@
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "11.2.6",
"idb-keyval": "6.2.4",
"intl-messageformat": "11.2.7",
"js-yaml": "4.1.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -106,7 +106,7 @@
"lit": "3.3.3",
"lit-html": "3.3.3",
"luxon": "3.7.2",
"marked": "18.0.3",
"marked": "18.0.4",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.4",
"object-hash": "3.0.0",
@@ -118,7 +118,7 @@
"sortablejs": "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch",
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"tinykeys": "4.0.0",
"weekstart": "2.0.0",
"workbox-cacheable-response": "7.4.1",
"workbox-core": "7.4.1",
@@ -141,7 +141,7 @@
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.11",
"@rspack/core": "2.0.3",
"@rspack/core": "2.0.4",
"@rspack/dev-server": "2.0.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.26",
@@ -160,7 +160,7 @@
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.1.6",
"@vitest/coverage-v8": "4.1.7",
"babel-loader": "10.1.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.4",
@@ -175,7 +175,7 @@
"eslint-plugin-wc": "3.1.0",
"fancy-log": "2.0.0",
"fs-extra": "11.3.5",
"generate-license-file": "4.1.1",
"generate-license-file": "4.2.1",
"glob": "13.0.6",
"globals": "17.6.0",
"gulp": "5.0.1",
@@ -201,9 +201,9 @@
"terser-webpack-plugin": "5.6.0",
"ts-lit-plugin": "2.0.2",
"typescript": "6.0.3",
"typescript-eslint": "8.59.3",
"typescript-eslint": "8.59.4",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.6",
"vitest": "4.1.7",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
@@ -219,8 +219,8 @@
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"glob@^10.2.2": "^10.5.0"
},
"packageManager": "[email protected]4.1",
"packageManager": "[email protected]5.0",
"volta": {
"node": "24.15.0"
"node": "24.16.0"
}
}
+15 -1
View File
@@ -1,6 +1,20 @@
import timezones from "google-timezones-json";
import { TimeZone } from "../../data/translation";
const RESOLVED_TIME_ZONE = Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
const RESOLVED_RAW = Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
// Some environments (e.g. Android emulator) return a UTC offset like "+00:00"
// instead of an IANA zone name. Only accept values that are known IANA zones,
// matching the list used by ha-timezone-picker.
const RESOLVED_TIME_ZONE =
RESOLVED_RAW &&
(RESOLVED_RAW === "UTC" ||
RESOLVED_RAW === "Etc/UTC" ||
RESOLVED_RAW in timezones)
? RESOLVED_RAW
: undefined;
export const HAS_RESOLVED_IANA_TIME_ZONE = RESOLVED_TIME_ZONE !== undefined;
// Browser time zone can be determined from Intl, with fallback to UTC for polyfill or no support.
export const LOCAL_TIME_ZONE = RESOLVED_TIME_ZONE ?? "UTC";
@@ -128,7 +128,9 @@ export class HaAutomationRow extends LitElement {
}
.row {
display: flex;
padding: 0 0 0 var(--ha-space-3);
padding-left: var(--ha-space-3);
padding-inline-start: var(--ha-space-3);
padding-inline-end: initial;
min-height: 48px;
align-items: flex-start;
cursor: pointer;
@@ -144,6 +146,8 @@ export class HaAutomationRow extends LitElement {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--ha-color-on-neutral-quiet);
margin-left: calc(var(--ha-space-2) * -1);
margin-inline-start: calc(var(--ha-space-2) * -1);
margin-inline-end: initial;
}
:host([building-block]) .leading-icon-wrapper {
background-color: var(--ha-color-fill-neutral-loud-resting);
+14 -7
View File
@@ -1,3 +1,4 @@
import { consume, type ContextType } from "@lit/context";
import {
mdiAmpersand,
mdiClockOutline,
@@ -12,11 +13,11 @@ import {
mdiWeatherSunny,
} from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import { computeDomain } from "../common/entity/compute_domain";
import { configContext, connectionContext } from "../data/context";
import { conditionIcon, FALLBACK_DOMAIN_ICONS } from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
@@ -36,12 +37,18 @@ export const CONDITION_ICONS = {
@customElement("ha-condition-icon")
export class HaConditionIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public condition?: string;
@property() public icon?: string;
@state()
@consume({ context: connectionContext, subscribe: true })
protected _connection?: ContextType<typeof connectionContext>;
@state()
@consume({ context: configContext, subscribe: true })
protected _config?: ContextType<typeof configContext>;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
@@ -51,13 +58,13 @@ export class HaConditionIcon extends LitElement {
return nothing;
}
if (!this.hass) {
if (!this._connection || !this._config) {
return this._renderFallback();
}
const icon = conditionIcon(
this.hass.connection,
this.hass.config,
this._connection.connection,
this._config.config,
this.condition
).then((icn) => {
if (icn) {
+1
View File
@@ -294,6 +294,7 @@ export class HaDrawer extends LitElement {
border-inline-end: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
box-sizing: border-box;
transition: width var(--ha-animation-duration-normal) ease;
z-index: 6;
}
.app-content {
+3
View File
@@ -109,6 +109,8 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
@property({ attribute: "custom-value-label" })
public customValueLabel?: string;
@property({ type: Boolean, attribute: "no-sort" }) public noSort = false;
@query(".container") private _containerElement?: HTMLDivElement;
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
@@ -271,6 +273,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
.selectedSection=${this.selectedSection}
.searchKeys=${this.searchKeys}
.customValueLabel=${this.customValueLabel}
.noSort=${this.noSort}
></ha-picker-combo-box>
`;
}
+3 -1
View File
@@ -167,6 +167,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@property({ type: Boolean, reflect: true }) public clearable = false;
@property({ type: Boolean, attribute: "no-sort" }) public noSort = false;
@query("lit-virtualizer") public virtualizerElement?: LitVirtualizer;
@query("ha-input-search") private _searchFieldElement?: HaInputSearch;
@@ -342,7 +344,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
private _getItems = () => {
let items = [...(this.getItems(this._search, this._selectedSection) || [])];
if (!this.sections?.length) {
if (!this.sections?.length && !this.noSort) {
items = items.sort((entityA, entityB) => {
const sortLabelA =
typeof entityA === "string" ? entityA : entityA.sorting_label;
@@ -199,6 +199,7 @@ export class HaSelectSelector extends LitElement {
: nothing}
<ha-generic-picker
no-sort
.hass=${this.hass}
.helper=${this.helper}
.disabled=${this.disabled}
@@ -215,6 +216,7 @@ export class HaSelectSelector extends LitElement {
if (this.selector.select?.custom_value) {
return html`
<ha-generic-picker
no-sort
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
+63 -37
View File
@@ -1,13 +1,17 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
const DEFAULT_THEME = "default";
const SEARCH_KEYS = [{ name: "primary", weight: 1 }];
@customElement("ha-theme-picker")
export class HaThemePicker extends LitElement {
@property() public value?: string;
@@ -25,52 +29,74 @@ export class HaThemePicker extends LitElement {
@property({ type: Boolean }) public required = false;
@property({ attribute: "no-theme-label" }) public noThemeLabel?: string;
private _getThemeOptions = memoizeOne(
(
themes: Record<string, unknown>,
locale: string,
includeDefault: boolean
): PickerComboBoxItem[] => {
const items: PickerComboBoxItem[] = [];
if (includeDefault) {
items.push({ id: DEFAULT_THEME, primary: "Home Assistant" });
}
const themeNames = Object.keys(themes).sort((a, b) =>
caseInsensitiveStringCompare(a, b, locale)
);
for (const theme of themeNames) {
items.push({ id: theme, primary: theme });
}
return items;
}
);
private _getItems = () =>
this._getThemeOptions(
this.hass?.themes.themes || {},
this.hass?.locale.language || "en",
this.includeDefault
);
private _valueRenderer = (value: string): TemplateResult =>
html`<span slot="headline"
>${this._getItems().find((i) => i.id === value)?.primary ?? value}</span
>`;
protected render(): TemplateResult {
const options: HaSelectOption[] = Object.keys(
this.hass?.themes.themes || {}
).map((theme) => ({
value: theme,
}));
if (this.includeDefault) {
options.unshift({
value: DEFAULT_THEME,
label: "Home Assistant",
});
}
if (!this.required) {
options.unshift({
value: "remove",
label: this.hass!.localize("ui.components.theme-picker.no_theme"),
});
}
return html`
<ha-select
.label=${this.label ||
this.hass!.localize("ui.components.theme-picker.theme")}
.value=${this.value}
<ha-generic-picker
.label=${this.label ??
this.hass?.localize("ui.components.theme-picker.theme") ??
"Theme"}
.placeholder=${this.noThemeLabel ??
this.hass?.localize("ui.components.theme-picker.no_theme")}
.helper=${this.helper}
.required=${this.required}
.value=${this.value}
.valueRenderer=${this._valueRenderer}
.getItems=${this._getItems}
.searchKeys=${SEARCH_KEYS}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
></ha-select>
.required=${this.required}
@value-changed=${this._changed}
popover-placement="bottom"
></ha-generic-picker>
`;
}
static styles = css`
ha-select {
ha-generic-picker {
width: 100%;
display: block;
}
`;
private _changed(ev: HaSelectSelectEvent): void {
if (!this.hass || ev.detail.value === "") {
return;
}
this.value = ev.detail.value === "remove" ? undefined : ev.detail.value;
private _changed(ev: ValueChangedEvent<string | undefined>): void {
ev.stopPropagation();
this.value = ev.detail.value;
fireEvent(this, "value-changed", { value: this.value });
}
}
+14 -4
View File
@@ -1,3 +1,4 @@
import { consume, type ContextType } from "@lit/context";
import {
mdiAvTimer,
mdiCalendar,
@@ -18,9 +19,10 @@ import {
mdiWebhook,
} from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import { computeDomain } from "../common/entity/compute_domain";
import { configContext, connectionContext } from "../data/context";
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import type { HomeAssistant } from "../types";
@@ -56,6 +58,14 @@ export class HaTriggerIcon extends LitElement {
@property() public icon?: string;
@state()
@consume({ context: connectionContext, subscribe: true })
protected _connection?: ContextType<typeof connectionContext>;
@state()
@consume({ context: configContext, subscribe: true })
protected _config?: ContextType<typeof configContext>;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
@@ -65,13 +75,13 @@ export class HaTriggerIcon extends LitElement {
return nothing;
}
if (!this.hass) {
if (!this._connection || !this._config) {
return this._renderFallback();
}
const icon = triggerIcon(
this.hass.connection,
this.hass.config,
this._connection.connection,
this._config.config,
this.trigger
).then((icn) => {
if (icn) {
+1
View File
@@ -112,6 +112,7 @@ export class HaTileContainer extends LitElement {
flex-direction: column;
text-align: center;
justify-content: center;
padding: 10px 0;
}
.vertical ::slotted([slot="info"]) {
width: 100%;
+24 -6
View File
@@ -171,16 +171,28 @@ export class HaTracePathDetails extends LitElement {
migrateAutomationTrigger({
...currentDetail,
}) as Trigger,
this.hass,
this._entityReg
this.hass.localize,
this.hass.locale,
this._entityReg,
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue
)}
</h2>`
: selectedType === "condition"
? html`<h2>
${describeCondition(
currentDetail,
this.hass,
this._entityReg
this.hass.localize,
this.hass.locale,
this._entityReg,
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue
)}
</h2>`
: selectedType === "action"
@@ -215,8 +227,14 @@ export class HaTracePathDetails extends LitElement {
.includes("condition")
? html`[${describeCondition(
currentDetail,
this.hass,
this._entityReg
this.hass.localize,
this.hass.locale,
this._entityReg,
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue
)}]<br />`
: nothing}
${this.hass!.localize(
+3 -3
View File
@@ -10,7 +10,7 @@ import type { WeekdayShort } from "../common/datetime/weekday";
import { navigate } from "../common/navigate";
import type { LocalizeKeys } from "../common/translations/localize";
import { createSearchParam } from "../common/url/search-params";
import type { Context, HomeAssistant } from "../types";
import type { CallWS, Context, HomeAssistant } from "../types";
import type { BlueprintInput } from "./blueprint";
import type { ConditionDescription } from "./condition";
import { CONDITION_BUILDING_BLOCKS } from "./condition";
@@ -578,11 +578,11 @@ export const subscribeTrigger = (
});
export const testCondition = (
hass: HomeAssistant,
callWS: CallWS,
condition: Condition | Condition[],
variables?: Record<string, unknown>
) =>
hass.callWS<{ result: boolean }>({
callWS<{ result: boolean }>({
type: "test_condition",
condition,
variables,
File diff suppressed because it is too large Load Diff
+11 -2
View File
@@ -17,6 +17,7 @@ export interface BluetoothDeviceData extends DataTableRowData {
source: string;
time: number;
tx_power: number;
raw: string | null;
}
export interface BluetoothConnectionData extends DataTableRowData {
@@ -58,13 +59,21 @@ export interface BluetoothAllocationsData {
allocated: string[];
}
export type BluetoothScannerMode = "active" | "passive";
export type BluetoothScannerRequestedMode = BluetoothScannerMode | "auto";
export interface BluetoothScannerState {
source: string;
adapter: string;
current_mode: "active" | "passive" | null;
requested_mode: "active" | "passive" | null;
current_mode: BluetoothScannerMode | null;
requested_mode: BluetoothScannerRequestedMode | null;
}
export const isScannerStateMismatch = (state: BluetoothScannerState): boolean =>
state.requested_mode !== "auto" &&
state.current_mode !== state.requested_mode;
export const subscribeBluetoothScannersDetailsUpdates = (
conn: Connection,
store: Store<BluetoothScannersDetails>
+3 -3
View File
@@ -1,4 +1,4 @@
import type { HomeAssistant } from "../types";
import type { CallWS } from "../types";
export interface ValidConfig {
valid: true;
@@ -13,10 +13,10 @@ export interface InvalidConfig {
type ValidKeys = "triggers" | "actions" | "conditions";
export const validateConfig = <T extends Partial<Record<ValidKeys, unknown>>>(
hass: HomeAssistant,
callWS: CallWS,
config: T
): Promise<Record<keyof T, ValidConfig | InvalidConfig>> =>
hass.callWS({
callWS({
type: "validate_config",
...config,
});
+4 -1
View File
@@ -5,7 +5,10 @@ export interface DataTableFilter {
export type DataTableFilters = Record<string, DataTableFilter>;
export type DataTableFiltersValue = string[] | { key: string[] } | undefined;
export type DataTableFiltersValue =
| string[]
| Record<"key" | string, string[]>
| undefined;
export type DataTableFiltersValues = Record<string, DataTableFiltersValue>;
+3 -3
View File
@@ -15,7 +15,7 @@ import {
mdiPlaylistMusic,
mdiPlayPause,
mdiPodcast,
mdiPower,
mdiPowerStandby,
mdiPowerOff,
mdiPowerOn,
mdiRepeat,
@@ -295,7 +295,7 @@ export const computeMediaControls = (
return supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON)
? [
{
icon: mdiPower,
icon: mdiPowerStandby,
action: "turn_on",
},
]
@@ -316,7 +316,7 @@ export const computeMediaControls = (
if (supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_OFF)) {
buttons.push({
icon: assumedState ? mdiPowerOff : mdiPower,
icon: assumedState ? mdiPowerOff : mdiPowerStandby,
action: "turn_off",
});
}
+12 -2
View File
@@ -9,8 +9,8 @@ import type { Condition } from "./automation";
import { describeCondition } from "./automation_i18n";
import { localizeDeviceAutomationAction } from "./device/device_automation";
import type { EntityRegistryEntry } from "./entity/entity_registry";
import { domainToName } from "./integration";
import type { DomainManifestLookup } from "./integration";
import { domainToName } from "./integration";
import type {
ActionType,
ActionTypes,
@@ -322,7 +322,17 @@ const tryDescribeAction = <T extends ActionType>(
return hass.localize(
`${actionTranslationBaseKey}.check_condition.description.full`,
{
condition: describeCondition(action as Condition, hass, entityRegistry),
condition: describeCondition(
action as Condition,
hass.localize,
hass.locale,
entityRegistry,
hass.states,
hass.entities,
hass.config,
hass.formatEntityState,
hass.formatEntityAttributeValue
),
}
);
}
+3
View File
@@ -3,6 +3,7 @@ import type { LocalizeFunc } from "../../common/translations/localize";
import { createSearchParam } from "../../common/url/search-params";
import type { SingleHassServiceTarget } from "../../data/target";
import {
ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM,
@@ -105,6 +106,8 @@ export function addToActionHandler(
searchParams[ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM] = target.entity_id;
} else if (target.device_id) {
searchParams[ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM] = target.device_id;
} else if (target.area_id) {
searchParams[ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM] = target.area_id;
}
const params = (addElement: string) =>
@@ -343,9 +343,9 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
this._step = this._previousSteps.pop()!;
}
private _goToNextStep(ev?: CustomEvent) {
private async _goToNextStep(ev?: CustomEvent) {
if (ev?.detail?.updateConfig) {
this._fetchAssistConfiguration();
await this._fetchAssistConfiguration();
}
if (ev?.detail?.nextStep) {
this._nextStep = ev.detail.nextStep;
+55 -23
View File
@@ -1,18 +1,25 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { LOCAL_TIME_ZONE } from "../common/datetime/resolve-time-zone";
import memoizeOne from "memoize-one";
import {
HAS_RESOLVED_IANA_TIME_ZONE,
LOCAL_TIME_ZONE,
} from "../common/datetime/resolve-time-zone";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert";
import "../components/ha-button";
import { COUNTRIES } from "../components/ha-country-picker";
import "../components/ha-form/ha-form";
import type { HaForm } from "../components/ha-form/ha-form";
import type { HaFormSchema } from "../components/ha-form/types";
import "../components/ha-spinner";
import type { ConfigUpdateValues } from "../data/core";
import { saveCoreConfig } from "../data/core";
import { countryCurrency } from "../data/currency";
import { onboardCoreConfigStep } from "../data/onboarding";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HomeAssistant } from "../types";
import { getLocalLanguage } from "../util/common-translation";
import "./onboarding-location";
@@ -28,7 +35,9 @@ class OnboardingCoreConfig extends LitElement {
private _elevation = "0";
private _timeZone: ConfigUpdateValues["time_zone"] = LOCAL_TIME_ZONE;
@state() private _timeZone: ConfigUpdateValues["time_zone"] = LOCAL_TIME_ZONE;
@state() private _timeZoneDetected = HAS_RESOLVED_IANA_TIME_ZONE;
private _language: ConfigUpdateValues["language"] = getLocalLanguage();
@@ -42,7 +51,29 @@ class OnboardingCoreConfig extends LitElement {
@state() private _skipCore = false;
@query("ha-country-picker") private _countryPicker?: HTMLElement;
@query("ha-form") private _form?: HaForm;
private _schema = memoizeOne((includeTimeZone: boolean): HaFormSchema[] => [
{
name: "country",
required: true,
selector: { country: null },
},
...(includeTimeZone
? ([
{
name: "time_zone",
required: true,
selector: { timezone: null },
},
] satisfies HaFormSchema[])
: []),
]);
private _computeLabel = (schema: HaFormSchema) =>
this.hass.localize(
`ui.panel.config.core.section.core.core_config.${schema.name}` as any
);
protected render(): TemplateResult {
if (!this._location) {
@@ -68,17 +99,17 @@ class OnboardingCoreConfig extends LitElement {
)}
</p>
<ha-country-picker
class="flex"
<ha-form
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.country"
) || "Country"}
required
.data=${{
country: this._country ?? "",
time_zone: this._timeZone,
}}
.schema=${this._schema(!this._timeZoneDetected)}
.computeLabel=${this._computeLabel}
.disabled=${this._working}
.value=${this._countryValue}
@value-changed=${this._handleCountryChanged}
></ha-country-picker>
@value-changed=${this._handleFormChanged}
></ha-form>
<div class="footer">
<ha-button @click=${this._save} .disabled=${this._working}>
@@ -99,12 +130,12 @@ class OnboardingCoreConfig extends LitElement {
});
}
private get _countryValue() {
return this._country || "";
}
private _handleCountryChanged(ev: ValueChangedEvent<string>) {
this._country = ev.detail.value;
private _handleFormChanged(ev: CustomEvent) {
const value = ev.detail.value as { country?: string; time_zone?: string };
this._country = value.country || undefined;
if (value.time_zone) {
this._timeZone = value.time_zone;
}
}
private async _locationChanged(ev) {
@@ -123,11 +154,12 @@ class OnboardingCoreConfig extends LitElement {
}
if (ev.detail.value.timezone) {
this._timeZone = ev.detail.value.timezone;
this._timeZoneDetected = true;
}
if (ev.detail.value.unit_system) {
this._unitSystem = ev.detail.value.unit_system;
}
if (this._country) {
if (this._country && this._timeZoneDetected) {
this._skipCore = true;
this._save(ev);
return;
@@ -145,11 +177,11 @@ class OnboardingCoreConfig extends LitElement {
fireEvent(this, "onboarding-progress", { increase: 0.5 });
await this.updateComplete;
setTimeout(() => this._countryPicker!.focus(), 100);
setTimeout(() => this._form?.focus(), 100);
}
private async _save(ev) {
if (!this._location || !this._country) {
if (!this._location || !this._country || !this._timeZone) {
return;
}
ev.preventDefault();
@@ -166,7 +198,7 @@ class OnboardingCoreConfig extends LitElement {
this._unitSystem || ["US", "MM", "LR"].includes(this._country)
? "us_customary"
: "metric",
time_zone: this._timeZone || "UTC",
time_zone: this._timeZone,
currency: this._currency || countryCurrency[this._country] || "EUR",
country: this._country,
language: this._language,
+3
View File
@@ -6,6 +6,7 @@ import { classMap } from "lit/directives/class-map";
import { createRef, ref } from "lit/directives/ref";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { IFRAME_SANDBOX } from "../../util/iframe";
import { navigate } from "../../common/navigate";
import { computeRouteTail } from "../../common/url/route";
import { nextRender } from "../../common/util/render-status";
@@ -136,6 +137,8 @@ class HaPanelApp extends LitElement {
})}
title=${this._addon.name}
src=${this._addon.ingress_url!}
.sandbox=${IFRAME_SANDBOX}
allow="microphone; camera; clipboard-write"
@load=${this._checkLoaded}
${ref(this._iframeRef)}
>
+14 -10
View File
@@ -190,16 +190,20 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
.label=${this.hass.localize("ui.common.refresh")}
@click=${this._handleRefresh}
></ha-icon-button>
${showPane && this.hass.user?.is_admin
? html`<ha-list slot="pane" multi}>${calendarItems}</ha-list>
<ha-list-item
graphic="icon"
slot="pane-footer"
@click=${this._addCalendar}
>
<ha-svg-icon .path=${mdiPlus} slot="graphic"></ha-svg-icon>
${this.hass.localize("ui.components.calendar.create_calendar")}
</ha-list-item>`
${showPane
? html`<ha-list slot="pane" multi>${calendarItems}</ha-list>${this
.hass.user?.is_admin
? html`<ha-list-item
graphic="icon"
slot="pane-footer"
@click=${this._addCalendar}
>
<ha-svg-icon .path=${mdiPlus} slot="graphic"></ha-svg-icon>
${this.hass.localize(
"ui.components.calendar.create_calendar"
)}
</ha-list-item>`
: nothing}`
: nothing}
<ha-full-calendar
add-fab
@@ -0,0 +1,225 @@
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { consume, type ContextType } from "@lit/context";
import { customElement, state } from "lit/decorators";
import {
mdiPalette,
mdiPlayCircleOutline,
mdiPlaylistCheck,
mdiRobotOutline,
mdiScriptTextOutline,
} from "@mdi/js";
import { computeAreaName } from "../../../common/entity/compute_area_name";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-adaptive-dialog";
import "../../../components/ha-list";
import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon";
import {
areasContext,
internationalizationContext,
} from "../../../data/context";
import type { SceneEntities } from "../../../data/scene";
import { showSceneEditor } from "../../../data/scene";
import {
addToActionHandler,
type AddToActionKey,
} from "../../../dialogs/more-info/add-to";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { AreaAddToDialogParams } from "./show-dialog-area-add-to";
@customElement("dialog-area-add-to")
class DialogAreaAddTo extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: areasContext, subscribe: true })
private _areas!: ContextType<typeof areasContext>;
@state() private _params?: AreaAddToDialogParams;
@state() private _open = false;
public showDialog(params: AreaAddToDialogParams): void {
this._params = params;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-adaptive-dialog
.open=${this._open}
header-title=${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.title"
)}
@closed=${this._dialogClosed}
>
${this._renderOptions()}
</ha-adaptive-dialog>
`;
}
private _renderOptions() {
if (!this._params) {
return nothing;
}
const area = this._areas[this._params.areaId];
const areaName = computeAreaName(area) || this._params.areaId;
return html`
<h3 class="section-header">
${this._i18n.localize(
"ui.panel.config.devices.automation.automations_heading"
)}
</h3>
<ha-list>
${this._renderActionItem(
"automation_trigger",
mdiRobotOutline,
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
areaName
)}
${this._renderActionItem(
"automation_condition",
mdiPlaylistCheck,
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
areaName
)}
${this._renderActionItem(
"automation_action",
mdiPlayCircleOutline,
"ui.dialogs.more_info_control.add_to.actions.automation_action",
areaName
)}
</ha-list>
<h3 class="section-header">
${this._i18n.localize("ui.panel.config.devices.script.scripts_heading")}
</h3>
<ha-list>
${this._renderActionItem(
"script_action",
mdiScriptTextOutline,
"ui.dialogs.more_info_control.add_to.actions.script_action",
areaName
)}
</ha-list>
${this._renderSceneSection(areaName)}
`;
}
private _renderSceneSection(areaName: string) {
if (!this._params?.entityIds.length) {
return nothing;
}
return html`
<h3 class="section-header">
${this._i18n.localize("ui.panel.config.devices.scene.scenes_heading")}
</h3>
<ha-list>
<ha-list-item
graphic="icon"
@click=${this._handleCreateScene}
data-dialog="close"
>
<ha-svg-icon slot="graphic" .path=${mdiPalette}></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.scene",
{ target: areaName }
)}
</ha-list-item>
</ha-list>
`;
}
private _renderActionItem(
key: AddToActionKey,
path: string,
translationKey:
| "ui.dialogs.more_info_control.add_to.actions.automation_trigger"
| "ui.dialogs.more_info_control.add_to.actions.automation_condition"
| "ui.dialogs.more_info_control.add_to.actions.automation_action"
| "ui.dialogs.more_info_control.add_to.actions.script_action",
areaName: string
) {
return html`
<ha-list-item
graphic="icon"
data-type=${key}
@click=${this._handleAction}
data-dialog="close"
>
<ha-svg-icon slot="graphic" .path=${path}></ha-svg-icon>
${this._i18n.localize(translationKey, { target: areaName })}
</ha-list-item>
`;
}
private _handleAction(ev: Event) {
if (!this._params) {
return;
}
const key = (ev.currentTarget as HTMLElement).dataset
.type as AddToActionKey;
this.closeDialog();
addToActionHandler(key, { area_id: this._params.areaId });
}
private _handleCreateScene() {
if (!this._params) {
return;
}
const entities: SceneEntities = {};
for (const entityId of this._params.entityIds) {
entities[entityId] = "";
}
this.closeDialog();
showSceneEditor({ entities }, this._params.areaId);
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-adaptive-dialog {
--dialog-content-padding: 0;
}
.section-header {
padding: var(--ha-space-2) var(--ha-space-4) 0;
margin: 0;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
color: var(--secondary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-area-add-to": DialogAreaAddTo;
}
}
+229 -21
View File
@@ -1,6 +1,21 @@
import { consume } from "@lit/context";
import { mdiDelete, mdiDotsVertical, mdiImagePlus, mdiPencil } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket/dist/types";
import {
mdiDelete,
mdiDevices,
mdiDotsVertical,
mdiImagePlus,
mdiPalette,
mdiPencil,
mdiPlus,
mdiRobot,
mdiScriptText,
mdiShape,
mdiTools,
} from "@mdi/js";
import type {
HassEntity,
UnsubscribeFunc,
} from "home-assistant-js-websocket/dist/types";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -10,7 +25,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { goBack } from "../../../common/navigate";
import { goBack, navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { slugify } from "../../../common/string/slugify";
import { groupBy } from "../../../common/util/group-by";
@@ -18,11 +33,13 @@ import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-list";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import {
@@ -38,6 +55,7 @@ import {
computeEntityRegistryName,
sortEntityRegistryByName,
} from "../../../data/entity/entity_registry";
import { subscribeLabFeature } from "../../../data/labs";
import type { SceneEntity } from "../../../data/scene";
import type { ScriptEntity } from "../../../data/script";
import type { RelatedResult } from "../../../data/search";
@@ -46,9 +64,15 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { isHelperDomain } from "../helpers/const";
import "../../logbook/ha-logbook";
import {
loadAreaAddToDialog,
showAreaAddToDialog,
} from "./show-dialog-area-add-to";
import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
@@ -59,8 +83,60 @@ declare interface NameAndEntity<EntityType extends HassEntity> {
entity: EntityType;
}
type AreaQuickLinkKey =
| "devices"
| "entities"
| "helpers"
| "automations"
| "scenes"
| "scripts";
const NAVIGATION_ACTIONS: {
value: string;
path: string;
icon: string;
countKey: AreaQuickLinkKey;
}[] = [
{
value: "navigate-devices",
path: "/config/devices/dashboard",
icon: mdiDevices,
countKey: "devices",
},
{
value: "navigate-entities",
path: "/config/entities",
icon: mdiShape,
countKey: "entities",
},
{
value: "navigate-helpers",
path: "/config/helpers",
icon: mdiTools,
countKey: "helpers",
},
{
value: "navigate-automations",
path: "/config/automation/dashboard",
icon: mdiRobot,
countKey: "automations",
},
{
value: "navigate-scenes",
path: "/config/scene/dashboard",
icon: mdiPalette,
countKey: "scenes",
},
{
value: "navigate-scripts",
path: "/config/script/dashboard",
icon: mdiScriptText,
countKey: "scripts",
},
] as const;
@customElement("ha-config-area-page")
class HaConfigAreaPage extends LitElement {
class HaConfigAreaPage extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public areaId!: string;
@@ -77,6 +153,8 @@ class HaConfigAreaPage extends LitElement {
@state() private _related?: RelatedResult;
@state() private _newTriggersConditions = false;
private _logbookTime = { recent: 86400 };
private _memberships = memoizeOne(
@@ -128,9 +206,35 @@ class HaConfigAreaPage extends LitElement {
.concat(memberships.indirectEntities.map((entry) => entry.entity_id))
);
private _getQuickLinkCounts = memoizeOne(
(
memberships: {
devices: DeviceRegistryEntry[];
entities: EntityRegistryEntry[];
indirectEntities: EntityRegistryEntry[];
},
related?: RelatedResult
) => {
const allEntityIds = this._allEntities(memberships);
const entityIds = related?.entity ?? allEntityIds;
return {
devices: related?.device?.length ?? memberships.devices.length,
entities: entityIds.length,
helpers: entityIds.filter((entityId) =>
isHelperDomain(computeDomain(entityId))
).length,
automations: related?.automation?.length ?? 0,
scenes: related?.scene?.length ?? 0,
scripts: related?.script?.length ?? 0,
};
}
);
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
loadAreaRegistryDetailDialog();
loadAreaAddToDialog();
}
protected updated(changedProps: PropertyValues<this>) {
@@ -140,6 +244,23 @@ class HaConfigAreaPage extends LitElement {
}
}
// When new_triggers_conditions labs feature is promoted, this whole method can be removed.
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
if (!isComponentLoaded(this.hass!.config, "automation")) {
return [];
}
return [
subscribeLabFeature(
this.hass!.connection,
"automation",
"new_triggers_conditions",
(feature) => {
this._newTriggersConditions = feature.enabled;
}
),
];
}
protected render() {
if (!this.hass.areas || !this.hass.devices || !this.hass.entities) {
return nothing;
@@ -162,6 +283,10 @@ class HaConfigAreaPage extends LitElement {
this._entityReg
);
const { devices, entities } = memberships;
const quickLinkCounts = this._getQuickLinkCounts(
memberships,
this._related
);
// Pre-compute the entity and device names, so we can sort by them
if (devices) {
@@ -245,6 +370,21 @@ class HaConfigAreaPage extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
${NAVIGATION_ACTIONS.map(
(action) => html`
<ha-dropdown-item value=${action.value}>
<ha-svg-icon slot="icon" .path=${action.icon}></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.areas.quick_links.${action.countKey}`,
{ count: quickLinkCounts[action.countKey] }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
`
)}
<wa-divider></wa-divider>
<ha-dropdown-item value="edit" .data=${area}>
<ha-svg-icon slot="icon" .path=${mdiPencil}> </ha-svg-icon>
${this.hass.localize("ui.panel.config.areas.edit_settings")}
@@ -271,15 +411,41 @@ class HaConfigAreaPage extends LitElement {
class="img-edit-btn"
></ha-icon-button>
</div>`
: html`<ha-button
appearance="filled"
size="small"
.entry=${area}
@click=${this._showSettings}
>
<ha-svg-icon .path=${mdiImagePlus} slot="start"></ha-svg-icon>
${this.hass.localize("ui.panel.config.areas.add_picture")}
</ha-button>`}
: nothing}
${area.picture && !this._newTriggersConditions
? nothing
: html`<div class="action-buttons">
${area.picture
? nothing
: html`<ha-button
appearance="filled"
.entry=${area}
@click=${this._showSettings}
>
<ha-svg-icon
.path=${mdiImagePlus}
slot="start"
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.areas.add_picture"
)}
</ha-button>`}
${this._newTriggersConditions
? html`<ha-button
appearance="filled"
variant="brand"
@click=${this._showAddToDialog}
>
<ha-svg-icon
slot="start"
.path=${mdiPlus}
></ha-svg-icon>
${this.hass.localize(
"ui.dialogs.more_info_control.add_to.title"
)}
</ha-button>`
: nothing}
</div>`}
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.devices.caption")}
@@ -612,9 +778,39 @@ class HaConfigAreaPage extends LitElement {
this._related = await findRelated(this.hass, "area", this.areaId);
}
private _handleMenuAction(ev: HaDropdownSelectEvent) {
private _showAddToDialog() {
const area = this.hass.areas[this.areaId];
if (!area) {
return;
}
showAreaAddToDialog(this, {
areaId: area.area_id,
entityIds: this._areaEntityIds,
});
}
private get _areaEntityIds(): string[] {
const memberships = this._memberships(
this.areaId,
Object.values(this.hass.devices),
this._entityReg
);
return this._allEntities(memberships);
}
private _handleMenuAction(
ev: HaDropdownSelectEvent<string, AreaRegistryEntry>
) {
const action = ev.detail?.item?.value;
const entry = (ev.detail?.item as any)?.data as AreaRegistryEntry;
const entry = ev.detail?.item?.data;
const navAction = NAVIGATION_ACTIONS.find((a) => a.value === action);
if (navAction) {
navigate(`${navAction.path}?historyBack=1&area=${this.areaId}`);
return;
}
switch (action) {
case "edit":
this._openDialog(entry);
@@ -625,15 +821,19 @@ class HaConfigAreaPage extends LitElement {
}
}
private _showSettings(ev: MouseEvent) {
const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry;
this._openDialog(entry);
private _showSettings(
ev: HASSDomCurrentTargetEvent<
HTMLButtonElement & { entry: AreaRegistryEntry }
>
) {
this._openDialog(ev.currentTarget.entry);
}
private _openEntity(ev) {
const entry: EntityRegistryEntry = (ev.currentTarget as any).entity;
private _openEntity(
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: EntityRegistryEntry }>
) {
showMoreInfoDialog(this, {
entityId: entry.entity_id,
entityId: ev.currentTarget.entity.entity_id,
});
}
@@ -675,6 +875,14 @@ class HaConfigAreaPage extends LitElement {
font-weight: var(--ha-font-weight-medium);
color: var(--secondary-text-color);
}
.action-buttons {
display: flex;
gap: var(--ha-space-2);
flex-wrap: wrap;
justify-content: space-around;
}
img {
border-radius: var(
--ha-card-border-radius,
@@ -0,0 +1,19 @@
import { fireEvent } from "../../../common/dom/fire_event";
export interface AreaAddToDialogParams {
areaId: string;
entityIds: string[];
}
export const loadAreaAddToDialog = () => import("./dialog-area-add-to");
export const showAreaAddToDialog = (
element: HTMLElement,
params: AreaAddToDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-area-add-to",
dialogImport: loadAreaAddToDialog,
dialogParams: params,
});
};
@@ -801,7 +801,7 @@ export default class HaAutomationActionRow extends LitElement {
this._running = false;
this._runResult = undefined;
const validated = await validateConfig(this.hass, {
const validated = await validateConfig(this.hass.callWS, {
actions: this.action,
});
@@ -143,6 +143,7 @@ export default class HaAutomationAction extends AutomationSortableListMixin<Acti
const addActionTargetFromQuery = getAddAutomationElementTargetFromQuery(
this.hass.states,
this.hass.devices,
this.hass.areas,
"action"
);
@@ -156,7 +156,6 @@ export class HaConditionAction
return html`<ha-condition-icon
slot="start"
.hass=${this.hass}
.condition=${condition}
></ha-condition-icon
><span slot="headline">${label}</span>`;
@@ -166,7 +165,6 @@ export class HaConditionAction
<ha-combo-box-item type="button">
<ha-condition-icon
slot="start"
.hass=${this.hass}
.condition=${item.search_labels!.condition || undefined}
></ha-condition-icon>
<span slot="headline">${item.primary}</span>
@@ -130,6 +130,7 @@ import "./add-automation-element/ha-automation-add-items";
import "./add-automation-element/ha-automation-add-search";
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
import {
ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
@@ -311,6 +312,7 @@ class DialogAddAutomationElement
const queryTarget = getAddAutomationElementTargetFromQuery(
this.hass.states,
this.hass.devices,
this.hass.areas,
params.type
);
this._openedFromQuery = !!queryTarget;
@@ -320,6 +322,7 @@ class DialogAddAutomationElement
searchParams.delete(ADD_AUTOMATION_ELEMENT_QUERY_PARAM);
searchParams.delete(ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM);
searchParams.delete(ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM);
searchParams.delete(ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM);
mainWindow.history.replaceState(
mainWindow.history.state,
"",
@@ -1894,12 +1897,7 @@ class DialogAddAutomationElement
): AddAutomationElementListItem {
const triggerName = getTriggerObjectId(trigger);
return {
icon: html`
<ha-trigger-icon
.hass=${this.hass}
.trigger=${trigger}
></ha-trigger-icon>
`,
icon: html` <ha-trigger-icon .trigger=${trigger}></ha-trigger-icon> `,
key: `${DYNAMIC_PREFIX}${trigger}`,
name:
localize(`component.${domain}.triggers.${triggerName}.name`) || trigger,
@@ -1917,10 +1915,7 @@ class DialogAddAutomationElement
const conditionName = getConditionObjectId(condition);
return {
icon: html`
<ha-condition-icon
.hass=${this.hass}
.condition=${condition}
></ha-condition-icon>
<ha-condition-icon .condition=${condition}></ha-condition-icon>
`,
key: `${DYNAMIC_PREFIX}${condition}`,
name:
@@ -233,7 +233,17 @@ export default class HaAutomationConditionRow extends LitElement {
this.condition as TriggerCondition
)
: capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
describeCondition(
this.condition,
this.hass.localize,
this.hass.locale,
this._entityReg,
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue
)
)}
${target !== undefined || (descriptionHasTarget && !this._isNew)
? this._renderTargets(
@@ -610,8 +620,14 @@ export default class HaAutomationConditionRow extends LitElement {
const triggerInfos = this._getTriggerInfos(
ensureArray(this._automationConfig?.triggers || []),
this.hass,
this._entityReg
this.hass.localize,
this.hass.locale,
this._entityReg,
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue
);
const infoById = new Map(triggerInfos.map((info) => [info.id, info]));
return html`${prefix}
@@ -858,7 +874,7 @@ export default class HaAutomationConditionRow extends LitElement {
let validateResult: Record<"conditions", InvalidConfig | ValidConfig>;
try {
validateResult = await validateConfig(this.hass, {
validateResult = await validateConfig(this.hass.callWS, {
conditions: condition,
});
} catch (err: any) {
@@ -889,7 +905,7 @@ export default class HaAutomationConditionRow extends LitElement {
let result: { result: boolean };
try {
result = await testCondition(this.hass, condition);
result = await testCondition(this.hass.callWS, condition);
} catch (err: any) {
if (this.condition !== condition) {
return;
@@ -921,7 +937,18 @@ export default class HaAutomationConditionRow extends LitElement {
),
inputType: "string",
placeholder: capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg, true)
describeCondition(
this.condition,
this.hass.localize,
this.hass.locale,
this._entityReg,
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue,
true
)
),
defaultValue: this.condition.alias,
confirmText: this.hass.localize("ui.common.submit"),
@@ -128,6 +128,7 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
const addConditionTargetFromQuery = getAddAutomationElementTargetFromQuery(
this.hass.states,
this.hass.devices,
this.hass.areas,
"condition"
);
@@ -1,4 +1,4 @@
import { consume } from "@lit/context";
import { consume, type ContextType } from "@lit/context";
import { mdiAlert } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -22,15 +22,19 @@ import {
getTriggerInfos,
type TriggerInfo,
} from "../../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../../data/context";
import {
configContext,
entitiesContext,
formattersContext,
fullEntitiesContext,
internationalizationContext,
statesContext,
} from "../../../../../data/context";
import type { EntityRegistryEntry } from "../../../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../../../types";
import "../../ha-trigger-id-chip";
@customElement("ha-automation-condition-trigger")
export class HaTriggerCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: TriggerCondition;
@property({ type: Boolean }) public disabled = false;
@@ -43,6 +47,22 @@ export class HaTriggerCondition extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
private _entityReg: EntityRegistryEntry[] = [];
@state()
@consume({ context: internationalizationContext, subscribe: true })
protected _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: statesContext, subscribe: true })
protected _states!: ContextType<typeof statesContext>;
@consume({ context: entitiesContext, subscribe: true })
protected _entities!: ContextType<typeof entitiesContext>;
@consume({ context: configContext, subscribe: true })
protected _config!: ContextType<typeof configContext>;
@consume({ context: formattersContext, subscribe: true })
protected _formatters!: ContextType<typeof formattersContext>;
private _triggerInfos = memoizeOne(
(
triggers: AutomationConfig["triggers"] | undefined,
@@ -50,8 +70,14 @@ export class HaTriggerCondition extends LitElement {
): TriggerInfo[] =>
getTriggerInfos(
triggers ? ensureArray(triggers) : undefined,
this.hass,
entityReg
this._i18n?.localize,
this._i18n?.locale,
entityReg,
this._states,
this._entities,
this._config?.config,
this._formatters?.formatEntityState,
this._formatters?.formatEntityAttributeValue
)
);
@@ -75,7 +101,7 @@ export class HaTriggerCondition extends LitElement {
if (!triggerInfos.length && !selectedIds.length) {
return html`
<ha-alert alert-type="info">
${this.hass.localize(
${this._i18n.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
)}
</ha-alert>
@@ -115,9 +141,9 @@ export class HaTriggerCondition extends LitElement {
>
${alertIcon}
</ha-trigger-id-chip>
${this.hass.localize("state.default.unavailable")}
${this._i18n.localize("state.default.unavailable")}
<ha-tooltip .for=${`trigger-${id}`}>
${this.hass.localize(
${this._i18n.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.unavailable_info",
{ id: html`<b>${id}</b>` }
)}
@@ -143,7 +169,7 @@ export class HaTriggerCondition extends LitElement {
</ha-trigger-id-chip>
${info.label}${info.count > 1
? html`<ha-tooltip .for=${`trigger-${info.id}`}
>${this.hass.localize(
>${this._i18n.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.duplicated_info"
)}</ha-tooltip
>`
@@ -647,7 +647,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
if (stateObj?.state !== UNAVAILABLE) {
return;
}
const validation = await validateConfig(this.hass, {
const validation = await validateConfig(this.hass.callWS, {
triggers: this.config.triggers,
conditions: this.config.conditions,
actions: this.config.actions,
@@ -768,10 +768,19 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
const hasUrlFilter =
this._searchParms.has("blueprint") || this._searchParms.has("label");
this._searchParms.has("area") ||
this._searchParms.has("blueprint") ||
this._searchParms.has("device") ||
this._searchParms.has("label");
if (!hasUrlFilter) {
this._filters = this._storageFilters;
}
if (this._searchParms.has("area")) {
this._filterArea();
}
if (this._searchParms.has("device")) {
this._filterDevice();
}
if (this._searchParms.has("blueprint")) {
this._filterBlueprint();
}
@@ -871,6 +880,38 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._filteredEntityIds = filteredEntityIds;
}
private _filterArea() {
const area = this._searchParms.get("area");
if (!area) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-floor-areas": {
value: { areas: [area] },
items: undefined,
},
};
this._applyFilters();
}
private _filterDevice() {
const device = this._searchParms.get("device");
if (!device) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-devices": {
value: [device],
items: undefined,
},
};
this._applyFilters();
}
private _filterLabel() {
const label = this._searchParms.get("label");
if (!label) {
@@ -117,7 +117,18 @@ export default class HaAutomationOptionRow extends LitElement {
if (typeof conditions[0] === "string") {
str += conditions[0];
} else {
str += describeCondition(conditions[0], this.hass, this._entityReg);
str += describeCondition(
conditions[0],
this.hass.localize,
this.hass.locale,
this._entityReg,
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue,
true
);
}
if (conditions.length > 1) {
str += this.hass.localize(
@@ -8,6 +8,7 @@ export const PASTE_VALUE = "__paste__";
export const ADD_AUTOMATION_ELEMENT_QUERY_PARAM = "add_automation_element";
export const ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM = "target_entity_id";
export const ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM = "target_device_id";
export const ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM = "target_area_id";
/** Parameters for the add automation element dialog. */
export interface AddAutomationElementDialogParams {
@@ -21,6 +22,7 @@ export interface AddAutomationElementDialogParams {
export const getAddAutomationElementTargetFromQuery = (
states: HomeAssistant["states"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
type: AddAutomationElementDialogParams["type"]
): SingleHassServiceTarget | undefined => {
const params = new URLSearchParams(window.location.search);
@@ -39,6 +41,11 @@ export const getAddAutomationElementTargetFromQuery = (
return { device_id: deviceId };
}
const areaId = params.get(ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM);
if (areaId && areas[areaId]) {
return { area_id: areaId };
}
return undefined;
};
@@ -273,7 +273,6 @@ export default class HaAutomationTriggerRow extends LitElement {
></ha-svg-icon>`
: html`<ha-trigger-icon
slot="leading-icon"
.hass=${this.hass}
.trigger=${(this.trigger as Exclude<Trigger, TriggerList>).trigger}
></ha-trigger-icon>`}
<h3 slot="header">
@@ -299,7 +298,17 @@ export default class HaAutomationTriggerRow extends LitElement {
</ha-tooltip>`
: nothing} `
: nothing}
${describeTrigger(this.trigger, this.hass, this._entityReg)}
${describeTrigger(
this.trigger,
this.hass.localize,
this.hass.locale,
this._entityReg,
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue
)}
${target !== undefined || (descriptionHasTarget && !this._isNew)
? this._renderTargets(
target,
@@ -675,7 +684,7 @@ export default class HaAutomationTriggerRow extends LitElement {
this._triggerUnsub = undefined;
}
const validateResult = await validateConfig(this.hass, {
const validateResult = await validateConfig(this.hass.callWS, {
triggers: trigger,
});
@@ -871,7 +880,18 @@ export default class HaAutomationTriggerRow extends LitElement {
),
inputType: "string",
placeholder: capitalizeFirstLetter(
describeTrigger(this.trigger, this.hass, this._entityReg, true)
describeTrigger(
this.trigger,
this.hass.localize,
this.hass.locale,
this._entityReg,
this.hass.states,
this.hass.entities,
this.hass.config,
this.hass.formatEntityState,
this.hass.formatEntityAttributeValue,
true
)
),
defaultValue: this.trigger.alias,
confirmText: this.hass.localize("ui.common.submit"),
@@ -311,6 +311,7 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
const addTriggerTargetFromQuery = getAddAutomationElementTargetFromQuery(
this.hass.states,
this.hass.devices,
this.hass.areas,
"trigger"
);
@@ -1,5 +1,5 @@
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { firstWeekdayIndex } from "../../../../../common/datetime/first_weekday";
@@ -225,6 +225,13 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
`ui.panel.config.automation.editor.triggers.type.time.${schema.name}`
);
};
static styles = css`
:host {
display: block;
margin-bottom: var(--ha-space-3);
}
`;
}
declare global {
@@ -1,11 +1,11 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-select";
import "../../../../../components/input/ha-input";
import type { HaInput } from "../../../../../components/input/ha-input";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/list/ha-list-base";
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
@@ -20,8 +20,8 @@ class HaBackupConfigAddon extends LitElement {
protected render() {
return html`
<ha-md-list>
<ha-md-list-item>
<ha-list-base>
<ha-list-item-base>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.backup.schedule.update_preference.label`
@@ -52,8 +52,8 @@ class HaBackupConfigAddon extends LitElement {
},
]}
></ha-select>
</ha-md-list-item>
<ha-md-list-item>
</ha-list-item-base>
<ha-list-item-base>
<span slot="headline">
${this.hass.localize(`ui.panel.config.backup.schedule.retention`)}
</span>
@@ -77,8 +77,8 @@ class HaBackupConfigAddon extends LitElement {
)}
</span>
</ha-input>
</ha-md-list-item>
</ha-md-list>
</ha-list-item-base>
</ha-list-base>
`;
}
@@ -106,13 +106,12 @@ class HaBackupConfigAddon extends LitElement {
}
static styles = css`
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
ha-list-base {
--ha-row-item-padding-inline: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
ha-list-item-base::part(headline),
ha-list-item-base::part(supporting-text) {
white-space: wrap;
}
ha-select {
min-width: 210px;
@@ -7,10 +7,10 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-switch";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/list/ha-list-base";
import type {
BackupAgent,
BackupAgentsConfig,
@@ -181,7 +181,7 @@ class HaBackupConfigAgents extends LitElement {
return html`
${allAgents.length > 0
? html`
<ha-md-list>
<ha-list-base>
${availableAgents.map((agent) => {
const agentId = agent.agent_id;
const name = computeBackupAgentName(
@@ -196,7 +196,7 @@ class HaBackupConfigAgents extends LitElement {
!this.cloudStatus.active_subscription;
return html`
<ha-md-list-item>
<ha-list-item-base>
${this._renderAgentIcon(agentId)}
<div slot="headline" class="name">${name}</div>
${description
@@ -220,7 +220,7 @@ class HaBackupConfigAgents extends LitElement {
!this._value.includes(agentId)}
@change=${this._agentToggled}
></ha-switch>
</ha-md-list-item>
</ha-list-item-base>
`;
})}
${unavailableAgents.length > 0 && this.showSettings
@@ -239,7 +239,7 @@ class HaBackupConfigAgents extends LitElement {
);
return html`
<ha-md-list-item>
<ha-list-item-base>
${this._renderAgentIcon(agentId)}
<div slot="headline" class="name">${name}</div>
<ha-icon-button
@@ -248,12 +248,12 @@ class HaBackupConfigAgents extends LitElement {
path=${mdiDelete}
@click=${this._deleteAgent}
></ha-icon-button>
</ha-md-list-item>
</ha-list-item-base>
`;
})}
`
: nothing}
</ha-md-list>
</ha-list-base>
`
: html`
<p>
@@ -293,30 +293,25 @@ class HaBackupConfigAgents extends LitElement {
}
static styles = css`
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
ha-list-base {
--ha-row-item-padding-inline: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
}
ha-md-list-item .name {
ha-list-item-base .name {
word-break: break-word;
}
ha-md-list-item img {
ha-list-item-base img {
width: 48px;
}
ha-md-list-item ha-svg-icon[slot="start"] {
ha-list-item-base ha-svg-icon[slot="start"] {
--mdc-icon-size: 48px;
color: var(--primary-text-color);
}
ha-md-list-item [slot="supporting-text"] {
display: flex;
align-items: center;
flex-direction: row;
ha-list-item-base::part(headline),
ha-list-item-base::part(supporting-text) {
white-space: wrap;
}
ha-list-item-base::part(end) {
gap: var(--ha-space-2);
line-height: var(--ha-line-height-condensed);
}
.unencrypted-warning {
display: flex;
@@ -338,7 +333,7 @@ class HaBackupConfigAgents extends LitElement {
.separator {
display: none;
}
ha-md-list-item [slot="supporting-text"] {
ha-list-item-base [slot="supporting-text"] {
display: flex;
align-items: flex-start;
flex-direction: column;
@@ -15,13 +15,13 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-select";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-switch";
import type { HaSwitch } from "../../../../../components/ha-switch";
import "../../../../../components/ha-tooltip";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/list/ha-list-base";
import { fetchHassioAddonsInfo } from "../../../../../data/hassio/addon";
import type { HostDisksUsage } from "../../../../../data/hassio/host";
import { fetchHostDisksUsage } from "../../../../../data/hassio/host";
@@ -238,8 +238,8 @@ class HaBackupConfigData extends LitElement {
return html`
${this._renderSizeEstimate()}
<ha-md-list>
<ha-md-list-item>
<ha-list-base>
<ha-list-item-base>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
<span slot="headline">
${this.hass.localize("ui.panel.config.backup.data.ha_settings")}
@@ -260,10 +260,10 @@ class HaBackupConfigData extends LitElement {
.checked=${data.homeassistant}
.disabled=${this.forceHomeAssistant || data.database}
></ha-switch>
</ha-md-list-item>
</ha-list-item-base>
${this._showDbOption
? html`<ha-md-list-item>
? html`<ha-list-item-base>
<ha-svg-icon slot="start" .path=${mdiChartBox}></ha-svg-icon>
<span slot="headline">
${this.hass.localize("ui.panel.config.backup.data.history")}
@@ -279,11 +279,11 @@ class HaBackupConfigData extends LitElement {
@change=${this._switchChanged}
.checked=${data.database}
></ha-switch>
</ha-md-list-item>`
</ha-list-item-base>`
: nothing}
${isHassio
? html`
<ha-md-list-item>
<ha-list-item-base>
<ha-svg-icon
slot="start"
.path=${mdiPlayBoxMultiple}
@@ -302,9 +302,9 @@ class HaBackupConfigData extends LitElement {
@change=${this._switchChanged}
.checked=${data.media}
></ha-switch>
</ha-md-list-item>
</ha-list-item-base>
<ha-md-list-item>
<ha-list-item-base>
<ha-svg-icon slot="start" .path=${mdiFolder}></ha-svg-icon>
<span slot="headline">
${this.hass.localize(
@@ -322,11 +322,11 @@ class HaBackupConfigData extends LitElement {
@change=${this._switchChanged}
.checked=${data.share}
></ha-switch>
</ha-md-list-item>
</ha-list-item-base>
${this._hasLocalAddons(this._addons)
? html`
<ha-md-list-item>
<ha-list-item-base>
<ha-svg-icon
slot="start"
.path=${mdiFolder}
@@ -347,12 +347,12 @@ class HaBackupConfigData extends LitElement {
@change=${this._switchChanged}
.checked=${data.local_addons}
></ha-switch>
</ha-md-list-item>
</ha-list-item-base>
`
: nothing}
${this._addons.length
? html`
<ha-md-list-item>
<ha-list-item-base>
<ha-svg-icon
slot="start"
.path=${mdiPuzzle}
@@ -392,12 +392,12 @@ class HaBackupConfigData extends LitElement {
},
]}
></ha-select>
</ha-md-list-item>
</ha-list-item-base>
`
: nothing}
`
: nothing}
</ha-md-list>
</ha-list-base>
${isHassio && this._showAddons && this._addons.length
? html`
<ha-expansion-panel
@@ -551,13 +551,15 @@ class HaBackupConfigData extends LitElement {
ha-spinner {
--ha-spinner-size: 24px;
}
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
ha-list-base {
--ha-row-item-padding-inline: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
ha-list-item-base::part(headline),
ha-list-item-base::part(supporting-text) {
white-space: wrap;
}
ha-list-item-base::part(start) {
color: var(--ha-color-text-secondary);
}
ha-select {
min-width: 210px;
@@ -2,8 +2,8 @@ import { mdiDownload } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/list/ha-list-base";
import type { HomeAssistant } from "../../../../../types";
import { showChangeBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-change-backup-encryption-key";
import { showSetBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-set-backup-encryption-key";
@@ -24,8 +24,8 @@ class HaBackupConfigEncryptionKey extends LitElement {
protected render() {
if (this._value) {
return html`
<ha-md-list>
<ha-md-list-item>
<ha-list-base>
<ha-list-item-base>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit"
@@ -47,8 +47,8 @@ class HaBackupConfigEncryptionKey extends LitElement {
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
)}
</ha-button>
</ha-md-list-item>
<ha-md-list-item>
</ha-list-item-base>
<ha-list-item-base>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.show_encryption_key"
@@ -69,8 +69,8 @@ class HaBackupConfigEncryptionKey extends LitElement {
"ui.panel.config.backup.encryption_key.show_encryption_key_action"
)}
</ha-button>
</ha-md-list-item>
<ha-md-list-item>
</ha-list-item-base>
<ha-list-item-base>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.change_encryption_key"
@@ -92,14 +92,14 @@ class HaBackupConfigEncryptionKey extends LitElement {
"ui.panel.config.backup.encryption_key.change_encryption_key_action"
)}
</ha-button>
</ha-md-list-item>
</ha-md-list>
</ha-list-item-base>
</ha-list-base>
`;
}
return html`
<ha-md-list>
<ha-md-list-item>
<ha-list-base>
<ha-list-item-base>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.set_encryption_key"
@@ -115,8 +115,8 @@ class HaBackupConfigEncryptionKey extends LitElement {
"ui.panel.config.backup.encryption_key.set_encryption_key_action"
)}</ha-button
>
</ha-md-list-item>
</ha-md-list>
</ha-list-item-base>
</ha-list-base>
`;
}
@@ -149,15 +149,13 @@ class HaBackupConfigEncryptionKey extends LitElement {
}
static styles = css`
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
ha-list-base {
--ha-row-item-padding-inline: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
ha-list-item-base::part(headline),
ha-list-item-base::part(supporting-text) {
white-space: wrap;
}
ha-button[size="small"] ha-svg-icon {
--mdc-icon-size: 16px;
}
@@ -3,11 +3,12 @@ import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { clamp } from "../../../../../common/number/clamp";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select";
import "../../../../../components/input/ha-input";
import type { HaInput } from "../../../../../components/input/ha-input";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/item/ha-row-item";
import type { BackupConfig, Retention } from "../../../../../data/backup";
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
@@ -104,7 +105,7 @@ class HaBackupConfigRetention extends LitElement {
}
return html`
<ha-md-list-item>
<ha-list-item-base>
<span slot="headline">
${this.headline ??
this.hass.localize(`ui.panel.config.backup.schedule.retention`)}
@@ -125,7 +126,7 @@ class HaBackupConfigRetention extends LitElement {
),
}))}
></ha-select>
</ha-md-list-item>
</ha-list-item-base>
${this._preset === RetentionPreset.CUSTOM
? html`<ha-expansion-panel
@@ -135,7 +136,7 @@ class HaBackupConfigRetention extends LitElement {
)}
outlined
>
<ha-md-list-item>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.custom_retention_label"
@@ -171,7 +172,7 @@ class HaBackupConfigRetention extends LitElement {
),
},
]}
></ha-select> </ha-md-list-item
></ha-select></ha-row-item
></ha-expansion-panel> `
: nothing}
`;
@@ -244,10 +245,17 @@ class HaBackupConfigRetention extends LitElement {
}
static styles = css`
ha-md-list-item {
--md-item-overflow: visible;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
ha-row-item,
ha-list-item-base {
--ha-row-item-padding-inline: 0;
}
ha-row-item::part(end) {
align-items: flex-start;
}
ha-list-item-base::part(headline),
ha-list-item-base::part(supporting-text) {
white-space: wrap;
}
ha-select {
min-width: 210px;
@@ -279,9 +287,6 @@ class HaBackupConfigRetention extends LitElement {
--expansion-panel-content-padding: 0 16px;
margin-bottom: 16px;
}
ha-md-list-item.days {
--md-item-align-items: flex-start;
}
`;
}
@@ -6,11 +6,12 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-select";
import "../../../../../components/ha-time-input";
import "../../../../../components/ha-tip";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/item/ha-row-item";
import "../../../../../components/list/ha-list-base";
import type {
BackupConfig,
BackupDay,
@@ -116,8 +117,8 @@ class HaBackupConfigSchedule extends LitElement {
const data = this._getData(this.value);
return html`
<ha-md-list>
<ha-md-list-item>
<ha-list-base>
<ha-list-item-base>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.schedule"
@@ -140,7 +141,7 @@ class HaBackupConfigSchedule extends LitElement {
),
}))}
></ha-select>
</ha-md-list-item>
</ha-list-item-base>
${data.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS
? html`<ha-expansion-panel
expanded
@@ -149,7 +150,7 @@ class HaBackupConfigSchedule extends LitElement {
)}
outlined
>
<ha-md-list-item class="days">
<ha-row-item class="days">
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.backup_every"
@@ -172,14 +173,14 @@ class HaBackupConfigSchedule extends LitElement {
`
)}
</div>
</ha-md-list-item>
</ha-row-item>
</ha-expansion-panel>`
: nothing}
${data.recurrence === BackupScheduleRecurrence.DAILY ||
(data.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS &&
data.days.length > 0)
? html`
<ha-md-list-item>
<ha-list-item-base>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.time"
@@ -214,7 +215,7 @@ class HaBackupConfigSchedule extends LitElement {
),
}))}
></ha-select>
</ha-md-list-item>
</ha-list-item-base>
${data.time_option === BackupScheduleTime.CUSTOM
? html`<ha-expansion-panel
expanded
@@ -223,7 +224,7 @@ class HaBackupConfigSchedule extends LitElement {
)}
outlined
>
<ha-md-list-item>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.schedule.custom_time_label"
@@ -248,14 +249,14 @@ class HaBackupConfigSchedule extends LitElement {
.locale=${this.hass.locale}
>
</ha-time-input>
</ha-md-list-item>
</ha-row-item>
</ha-expansion-panel>`
: nothing}
`
: nothing}
${this.supervisor
? html`
<ha-md-list-item>
<ha-list-item-base>
<span slot="headline">
${this.hass.localize(
`ui.panel.config.backup.schedule.update_preference.label`
@@ -286,7 +287,7 @@ class HaBackupConfigSchedule extends LitElement {
},
]}
></ha-select>
</ha-md-list-item>
</ha-list-item-base>
`
: nothing}
@@ -308,7 +309,7 @@ class HaBackupConfigSchedule extends LitElement {
>`,
})}</ha-tip
>
</ha-md-list>
</ha-list-base>
`;
}
@@ -398,13 +399,14 @@ class HaBackupConfigSchedule extends LitElement {
}
static styles = css`
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
ha-list-base {
--ha-row-item-padding-inline: 0;
}
ha-md-list-item {
--md-item-overflow: visible;
ha-row-item::part(headline),
ha-row-item::part(supporting-text),
ha-list-item-base::part(headline),
ha-list-item-base::part(supporting-text) {
white-space: wrap;
}
ha-select {
min-width: 210px;
@@ -430,8 +432,8 @@ class HaBackupConfigSchedule extends LitElement {
text-align: unset;
margin: 16px 0;
}
ha-md-list-item.days {
--md-item-align-items: flex-start;
ha-row-item-base.days::part(end) {
align-items: flex-start;
}
a {
color: var(--primary-color);
@@ -2,8 +2,6 @@ import memoizeOne from "memoize-one";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-card";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-button";
import "./ha-backup-data-picker";
import type { HomeAssistant } from "../../../../types";
@@ -120,22 +118,6 @@ class HaBackupDetailsRestore extends LitElement {
display: flex;
justify-content: flex-end;
}
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-list-item-two-line-container-height: 64px;
}
ha-md-list-item [slot="supporting-text"] {
display: flex;
align-items: center;
flex-direction: row;
gap: var(--ha-space-2);
line-height: var(--ha-line-height-condensed);
}
`;
}
@@ -4,8 +4,8 @@ import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/item/ha-list-item-base";
import "../../../../components/list/ha-list-base";
import {
computeBackupSize,
computeBackupType,
@@ -59,8 +59,8 @@ class HaBackupDetailsSummary extends LitElement {
</div>
<div class="card-content">
${errors.length ? this._renderErrorSummary(errors) : nothing}
<ha-md-list class="summary">
<ha-md-list-item>
<ha-list-base class="summary">
<ha-list-item-base>
<span slot="headline">
${this.hass.localize("ui.panel.config.backup.backup_type")}
</span>
@@ -69,8 +69,8 @@ class HaBackupDetailsSummary extends LitElement {
`ui.panel.config.backup.type.${computeBackupType(this.backup, this.isHassio)}`
)}
</span>
</ha-md-list-item>
<ha-md-list-item>
</ha-list-item-base>
<ha-list-item-base>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.details.summary.size"
@@ -79,16 +79,16 @@ class HaBackupDetailsSummary extends LitElement {
<span slot="supporting-text">
${bytesToString(computeBackupSize(this.backup))}
</span>
</ha-md-list-item>
<ha-md-list-item>
</ha-list-item-base>
<ha-list-item-base>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.details.summary.created"
)}
</span>
<span slot="supporting-text">${formattedDate}</span>
</ha-md-list-item>
</ha-md-list>
</ha-list-item-base>
</ha-list-base>
</div>
</ha-card>
`;
@@ -148,23 +148,17 @@ class HaBackupDetailsSummary extends LitElement {
display: flex;
justify-content: flex-end;
}
ha-md-list {
background: none;
padding: 0;
ha-list-base {
--ha-row-item-padding-inline: 0;
padding-bottom: var(--ha-space-3);
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-list-item-two-line-container-height: 64px;
ha-list-base.summary ha-list-item-base::part(headline) {
font-size: var(--ha-font-size-s);
color: var(--ha-color-text-secondary);
}
ha-md-list.summary ha-md-list-item {
--md-list-item-supporting-text-size: 1rem;
--md-list-item-label-text-size: 0.875rem;
--md-list-item-label-text-color: var(--secondary-text-color);
--md-list-item-supporting-text-color: var(--primary-text-color);
}
ha-md-list-item [slot="supporting-text"] {
ha-list-item-base [slot="supporting-text"] {
font-size: var(--ha-font-size-m);
color: var(--ha-color-text-primary);
display: flex;
align-items: center;
flex-direction: row;
@@ -4,9 +4,9 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-nav";
import {
getSupervisorUpdateConfig,
type SupervisorUpdateConfig,
@@ -73,11 +73,8 @@ class HaBackupOverviewAppUpdateBackup extends LitElement {
)}
</div>
<div class="card-content">
<ha-md-list>
<ha-md-list-item
type="link"
href="/config/backup/app-update-backups"
>
<ha-list-nav>
<ha-list-item-button href="/config/backup/app-update-backups">
<ha-svg-icon slot="start" .path=${mdiPuzzle}></ha-svg-icon>
<div slot="headline">${this._appUpdateBackupDescription()}</div>
<div slot="supporting-text">
@@ -86,8 +83,8 @@ class HaBackupOverviewAppUpdateBackup extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
</ha-list-item-button>
</ha-list-nav>
</div>
</ha-card>
`;
@@ -106,11 +103,6 @@ class HaBackupOverviewAppUpdateBackup extends LitElement {
padding-right: 0;
padding-top: 0;
}
ha-md-list {
padding-top: 0;
padding-bottom: 0;
}
`,
];
}
@@ -7,8 +7,8 @@ import { isComponentLoaded } from "../../../../../common/config/is_component_loa
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-nav";
import type { BackupContent, BackupType } from "../../../../../data/backup";
import {
computeBackupSize,
@@ -69,13 +69,10 @@ class HaBackupOverviewBackups extends LitElement {
${this.hass.localize("ui.panel.config.backup.overview.backups.title")}
</div>
<div class="card-content">
<ha-md-list>
<ha-list-nav>
${stats.map(
([type, { count, size }]) => html`
<ha-md-list-item
type="link"
href="/config/backup/backups?type=${type}"
>
<ha-list-item-button href="/config/backup/backups?type=${type}">
<ha-svg-icon
slot="start"
.path=${TYPE_ICONS[type]}
@@ -93,10 +90,10 @@ class HaBackupOverviewBackups extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-list-item-button>
`
)}
</ha-md-list>
</ha-list-nav>
</div>
<div class="card-actions">
<ha-button appearance="filled" href="/config/backup/backups?type=all">
@@ -134,6 +131,9 @@ class HaBackupOverviewBackups extends LitElement {
padding-right: 0;
padding-bottom: 0;
}
ha-list-item-button::part(start) {
color: var(--ha-color-text-secondary);
}
`,
];
}
@@ -5,10 +5,10 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/list/ha-list-base";
import type { BackupAgent } from "../../../../../data/backup";
import {
computeBackupAgentName,
@@ -332,7 +332,7 @@ export class HaBackupOverviewProgress extends LitElement {
? this._handleAgentCollapseEnd
: undefined}
>
<ha-md-list class="agent-list">
<ha-list-base class="agent-list">
${this.agents.map((agent) => {
const name = computeBackupAgentName(
this.hass.localize,
@@ -344,7 +344,7 @@ export class HaBackupOverviewProgress extends LitElement {
if (agentPercent !== undefined) {
if (agentPercent >= 100) {
return html`
<ha-md-list-item>
<ha-list-item-base>
${this._renderAgentIcon(agent.agent_id)}
<div slot="headline">${name}</div>
<div slot="supporting-text">
@@ -357,11 +357,11 @@ export class HaBackupOverviewProgress extends LitElement {
class="agent-complete"
.path=${mdiCheck}
></ha-svg-icon>
</ha-md-list-item>
</ha-list-item-base>
`;
}
return html`
<ha-md-list-item>
<ha-list-item-base>
${this._renderAgentIcon(agent.agent_id)}
<div slot="headline">${name}</div>
<div slot="supporting-text">
@@ -373,12 +373,12 @@ export class HaBackupOverviewProgress extends LitElement {
${agentPercent}%
</span>
<ha-spinner slot="end" size="tiny"></ha-spinner>
</ha-md-list-item>
</ha-list-item-base>
`;
}
return html`
<ha-md-list-item>
<ha-list-item-base>
${this._renderAgentIcon(agent.agent_id)}
<div slot="headline">${name}</div>
<div slot="supporting-text">
@@ -387,10 +387,10 @@ export class HaBackupOverviewProgress extends LitElement {
)}
</div>
<ha-spinner slot="end" size="tiny"></ha-spinner>
</ha-md-list-item>
</ha-list-item-base>
`;
})}
</ha-md-list>
</ha-list-base>
</div>
`;
}
@@ -506,14 +506,13 @@ export class HaBackupOverviewProgress extends LitElement {
margin-top: var(--ha-space-4);
overflow: hidden;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
ha-list-item-base {
--ha-row-item-padding-inline: 0;
}
ha-md-list-item img {
ha-list-item-base img {
width: 48px;
}
ha-md-list-item ha-svg-icon[slot="start"] {
ha-list-item-base ha-svg-icon[slot="start"] {
--mdc-icon-size: 48px;
color: var(--primary-text-color);
}
@@ -521,7 +520,7 @@ export class HaBackupOverviewProgress extends LitElement {
font-size: var(--ha-font-size-s);
color: var(--secondary-text-color);
}
ha-md-list-item [slot="supporting-text"] {
ha-list-item-base::part(supporting-text) {
display: flex;
align-items: center;
}
@@ -7,9 +7,9 @@ import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-button";
import "../../../../../components/list/ha-list-nav";
import type { BackupAgent, BackupConfig } from "../../../../../data/backup";
import {
BackupScheduleRecurrence,
@@ -213,11 +213,8 @@ class HaBackupBackupsSummary extends LitElement {
)}
</div>
<div class="card-content">
<ha-md-list>
<ha-md-list-item
type="link"
href="/config/backup/settings#schedule"
>
<ha-list-nav>
<ha-list-item-button href="/config/backup/settings#schedule">
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<div slot="headline">
${this._scheduleDescription(this.config)}
@@ -228,8 +225,8 @@ class HaBackupBackupsSummary extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item type="link" href="/config/backup/settings#data">
</ha-list-item-button>
<ha-list-item-button href="/config/backup/settings#data">
<ha-svg-icon slot="start" .path=${mdiDatabase}></ha-svg-icon>
<div slot="headline">
${this._showDbOption &&
@@ -247,13 +244,10 @@ class HaBackupBackupsSummary extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-list-item-button>
${isHassio
? html`
<ha-md-list-item
type="link"
href="/config/backup/settings#data"
>
<ha-list-item-button href="/config/backup/settings#data">
<ha-svg-icon slot="start" .path=${mdiPuzzle}></ha-svg-icon>
<div slot="headline">
${this._addonsDescription(this.config)}
@@ -264,13 +258,10 @@ class HaBackupBackupsSummary extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-list-item-button>
`
: nothing}
<ha-md-list-item
type="link"
href="/config/backup/settings#locations"
>
<ha-list-item-button href="/config/backup/settings#locations">
<ha-svg-icon slot="start" .path=${mdiUpload}></ha-svg-icon>
<div slot="headline">
${this._locationsDescription(this.config)}
@@ -281,8 +272,8 @@ class HaBackupBackupsSummary extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
</ha-list-item-button>
</ha-list-nav>
</div>
<div class="card-actions">
<ha-button @click=${this._configure} appearance="filled">
@@ -321,6 +312,9 @@ class HaBackupBackupsSummary extends LitElement {
padding-right: 0;
padding-bottom: 0;
}
ha-list-item-button::part(start) {
color: var(--ha-color-text-secondary);
}
`,
];
}
@@ -13,9 +13,9 @@ import type { LocalizeKeys } from "../../../../../common/translations/localize";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/list/ha-list-base";
import type { BackupConfig, BackupContent } from "../../../../../data/backup";
import {
BackupScheduleRecurrence,
@@ -65,15 +65,15 @@ class HaBackupOverviewBackups extends LitElement {
) {
return html`
<ha-backup-summary-card .heading=${heading} .status=${status}>
<ha-md-list>
<ha-md-list-item>
<ha-list-base>
<ha-list-item-base>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline" class=${headline === null ? "skeleton" : ""}
>${headline}</span
>
</ha-md-list-item>
</ha-list-item-base>
${description || description === null
? html`<ha-md-list-item>
? html`<ha-list-item-base>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span
slot="headline"
@@ -90,9 +90,9 @@ class HaBackupOverviewBackups extends LitElement {
.path=${mdiInformationOutline}
></ha-icon-button>`
: nothing}
</ha-md-list-item>`
</ha-list-item-base>`
: nothing}
</ha-md-list>
</ha-list-base>
</ha-backup-summary-card>
`;
}
@@ -347,13 +347,12 @@ class HaBackupOverviewBackups extends LitElement {
justify-content: flex-end;
border-top: none;
}
ha-md-list {
background: none;
ha-list-item-base {
--ha-row-item-padding-block: var(--ha-space-2);
--ha-row-item-min-height: 40x;
}
ha-md-list-item {
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
--md-list-item-one-line-container-height: 40x;
ha-list-item-base::part(start) {
color: var(--ha-color-text-secondary);
}
span.skeleton {
position: relative;
@@ -11,9 +11,10 @@ import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/item/ha-list-item-button";
import "../../../../components/item/ha-row-item";
import "../../../../components/list/ha-list-base";
import type {
BackupConfig,
BackupMutableConfig,
@@ -366,36 +367,34 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
@click=${this._copyKeyToClipboard}
></ha-icon-button>
</div>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
)}
</span>
<ha-button
size="small"
appearance="plain"
slot="end"
@click=${this._downloadKey}
>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
)}
</ha-button>
</ha-md-list-item>
</ha-md-list>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
)}
</span>
<ha-button
size="small"
appearance="plain"
slot="end"
@click=${this._downloadKey}
>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
)}
</ha-button>
</ha-row-item>
`;
case "setup":
return html`
<ha-md-list class="full">
<ha-md-list-item type="button" @click=${this._useRecommended}>
<ha-list-base class="full">
<ha-list-item-button @click=${this._useRecommended}>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.dialogs.onboarding.setup.recommended_heading"
@@ -407,8 +406,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item type="button" @click=${this._nextStep}>
</ha-list-item-button>
<ha-list-item-button @click=${this._nextStep}>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.dialogs.onboarding.setup.custom_heading"
@@ -420,8 +419,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
</ha-list-item-button>
</ha-list-base>
`;
case "schedule":
return html`
@@ -547,16 +546,12 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
--ha-dialog-max-height: min(605px, 100% - 48px);
}
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
ha-md-list.full {
--md-list-item-leading-space: 24px;
--md-list-item-trailing-space: 24px;
margin-left: -24px;
margin-right: -24px;
ha-list-base.full {
--ha-row-item-padding-inline: var(--ha-space-6);
margin: 0 calc(-1 * var(--ha-space-6));
}
p {
margin-top: 0;
@@ -5,12 +5,11 @@ import { customElement, property, query, 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";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/item/ha-row-item";
import {
downloadEmergencyKit,
generateEncryptionKey,
@@ -160,26 +159,28 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
@click=${this._copyOldKeyToClipboard}
></ha-icon-button>
</div>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_old_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_old_emergency_kit_description"
)}
</span>
<ha-button slot="end" @click=${this._downloadOld}>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_old_emergency_kit_action"
)}
</ha-button>
</ha-md-list-item>
</ha-md-list>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_old_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_old_emergency_kit_description"
)}
</span>
<ha-button
slot="end"
appearance="filled"
@click=${this._downloadOld}
>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_old_emergency_kit_action"
)}
</ha-button>
</ha-row-item>
`;
case "new":
return html`
@@ -195,26 +196,28 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
@click=${this._copyKeyToClipboard}
></ha-icon-button>
</div>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
)}
</span>
<ha-button slot="end" @click=${this._downloadNew}>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
)}
</ha-button>
</ha-md-list-item>
</ha-md-list>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
)}
</span>
<ha-button
slot="end"
appearance="filled"
@click=${this._downloadNew}
>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
)}
</ha-button>
</ha-row-item>
`;
case "done":
return html`
@@ -281,10 +284,8 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
ha-dialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
}
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
.encryption-key {
border: 1px solid var(--divider-color);
@@ -12,11 +12,10 @@ import "../../../../components/ha-dialog-header";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-select";
import "../../../../components/input/ha-input";
import type { HaInput } from "../../../../components/input/ha-input";
import "../../../../components/item/ha-row-item";
import type {
BackupAgent,
BackupConfig,
@@ -296,41 +295,39 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
@change=${this._nameChanged}
>
</ha-input>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.dialogs.generate.sync.locations"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.dialogs.generate.sync.locations_description"
)}
</span>
<ha-select
slot="end"
@selected=${this._selectChanged}
.value=${this._formData.agents_mode}
.options=${[
{
value: "all",
label: this.hass.localize(
"ui.panel.config.backup.dialogs.generate.sync.locations_options.all",
{ count: this._allAgentIds.length }
),
disabled: !!disabledAgentIds.length,
},
{
value: "custom",
label: this.hass.localize(
"ui.panel.config.backup.dialogs.generate.sync.locations_options.custom"
),
},
]}
></ha-select>
</ha-md-list-item>
</ha-md-list>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.dialogs.generate.sync.locations"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.dialogs.generate.sync.locations_description"
)}
</span>
<ha-select
slot="end"
@selected=${this._selectChanged}
.value=${this._formData.agents_mode}
.options=${[
{
value: "all",
label: this.hass.localize(
"ui.panel.config.backup.dialogs.generate.sync.locations_options.all",
{ count: this._allAgentIds.length }
),
disabled: !!disabledAgentIds.length,
},
{
value: "custom",
label: this.hass.localize(
"ui.panel.config.backup.dialogs.generate.sync.locations_options.custom"
),
},
]}
></ha-select>
</ha-row-item>
${disabledAgentIds.length
? html`
<ha-alert
@@ -443,24 +440,19 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
ha-dialog {
--dialog-content-padding: 24px;
}
ha-md-list {
background: none;
padding: 0;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
}
ha-md-list-item ha-select {
ha-row-item ha-select {
min-width: 210px;
}
@media all and (max-width: 450px) {
ha-md-list-item ha-select {
ha-row-item ha-select {
min-width: 160px;
width: 160px;
}
}
ha-md-list-item ha-select > span {
ha-row-item ha-select > span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@@ -3,11 +3,11 @@ import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-dialog";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-svg-icon";
import "../../../../components/item/ha-list-item-button";
import "../../../../components/list/ha-list-base";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -52,7 +52,7 @@ class DialogNewBackup extends LitElement implements HassDialog {
)}
@closed=${this._dialogClosed}
>
<ha-md-list
<ha-list-base
innerRole="listbox"
itemRoles="option"
.innerAriaLabel=${this.hass.localize(
@@ -60,9 +60,8 @@ class DialogNewBackup extends LitElement implements HassDialog {
)}
rootTabbable
>
<ha-md-list-item
<ha-list-item-button
@click=${this._automatic}
type="button"
.disabled=${!this._params.config.create_backup.password}
>
<ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon>
@@ -77,8 +76,8 @@ class DialogNewBackup extends LitElement implements HassDialog {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item @click=${this._manual} type="button">
</ha-list-item-button>
<ha-list-item-button @click=${this._manual}>
<ha-svg-icon slot="start" .path=${mdiGestureTap}></ha-svg-icon>
<span slot="headline">
${this.hass.localize(
@@ -91,8 +90,8 @@ class DialogNewBackup extends LitElement implements HassDialog {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
</ha-list-item-button>
</ha-list-base>
</ha-dialog>
`;
}
@@ -115,10 +114,6 @@ class DialogNewBackup extends LitElement implements HassDialog {
ha-dialog {
--dialog-content-padding: 0;
}
ha-md-list {
background: none;
}
ha-icon-next {
width: 24px;
}
@@ -8,8 +8,7 @@ import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/item/ha-row-item";
import {
downloadEmergencyKit,
generateEncryptionKey,
@@ -135,31 +134,29 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
@click=${this._copyKeyToClipboard}
></ha-icon-button>
</div>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
)}
</span>
<ha-button
size="small"
appearance="plain"
slot="end"
@click=${this._download}
>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
)}
</ha-button>
</ha-md-list-item>
</ha-md-list>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
)}
</span>
<ha-button
size="small"
appearance="plain"
slot="end"
@click=${this._download}
>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
)}
</ha-button>
</ha-row-item>
`;
case "done":
return html`
@@ -209,10 +206,8 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
ha-dialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
}
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
.encryption-key {
border: 1px solid var(--divider-color);
@@ -5,12 +5,11 @@ import { customElement, property, query, 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";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/item/ha-row-item";
import { downloadEmergencyKit } from "../../../../data/backup";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
@@ -74,26 +73,24 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
@click=${this._copyKeyToClipboard}
></ha-icon-button>
</div>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
)}
</span>
<ha-button slot="end" @click=${this._download}>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
)}
</ha-button>
</ha-md-list-item>
</ha-md-list>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
)}
</span>
<ha-button slot="end" appearance="filled" @click=${this._download}>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
)}
</ha-button>
</ha-row-item>
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
@@ -128,10 +125,8 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
ha-dialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
}
ha-md-list {
background: none;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
.encryption-key {
border: 1px solid var(--divider-color);
@@ -12,16 +12,17 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { navigate } from "../../../common/navigate";
import "../../../components/animation/ha-fade-in";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/animation/ha-fade-in";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/item/ha-list-item-base";
import "../../../components/list/ha-list-base";
import type {
BackupAgent,
BackupConfig,
@@ -44,7 +45,6 @@ import "./components/ha-backup-details-restore";
import "./components/ha-backup-details-summary";
import { showRestoreBackupDialog } from "./dialogs/show-dialog-restore-backup";
import { downloadBackup } from "./helper/download_backup";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
interface Agent extends BackupContentAgent {
id: string;
@@ -171,7 +171,7 @@ class HaConfigBackupDetails extends LitElement {
)}
</div>
<div class="card-content">
<ha-md-list>
<ha-list-base>
${this._agents.map((agent) => {
const agentId = agent.id;
@@ -186,7 +186,7 @@ class HaConfigBackupDetails extends LitElement {
const unencrypted = !agent.protected;
return html`
<ha-md-list-item>
<ha-list-item-base>
${
isLocalAgent(agentId)
? html`
@@ -282,10 +282,10 @@ class HaConfigBackupDetails extends LitElement {
`
: nothing
}
</ha-md-list-item>
</ha-list-item-base>
`;
})}
</ha-md-list>
</ha-list-base>
</div>
</ha-card>
`}
@@ -374,26 +374,20 @@ class HaConfigBackupDetails extends LitElement {
display: flex;
justify-content: flex-end;
}
ha-md-list {
background: none;
padding: 0;
ha-list-item-base {
--ha-row-item-padding-inline: 0;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-list-item-two-line-container-height: 64px;
}
ha-md-list-item img {
ha-list-item-base img {
width: 48px;
}
ha-md-list-item ha-svg-icon[slot="start"] {
ha-list-item-base ha-svg-icon[slot="start"] {
--mdc-icon-size: 48px;
color: var(--primary-text-color);
}
ha-button.danger {
--mdc-theme-primary: var(--error-color);
}
ha-md-list-item [slot="supporting-text"] {
ha-list-item-base [slot="supporting-text"] {
display: flex;
align-items: center;
flex-direction: row;
@@ -2,15 +2,14 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/animation/ha-fade-in";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/animation/ha-fade-in";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-switch";
import "../../../components/item/ha-row-item";
import type {
BackupAgent,
BackupAgentConfig,
@@ -143,10 +142,36 @@ class HaConfigBackupDetails extends LitElement {
"ui.panel.config.backup.location.encryption.description"
)}
</p>
<ha-md-list>
${CLOUD_AGENT === this.agentId
${CLOUD_AGENT === this.agentId
? html`
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_description"
)}
</span>
<ha-button
href="https://www.nabucasa.com/config/backups/"
target="_blank"
slot="end"
rel="noreferrer noopener"
appearance="plain"
size="small"
>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_learn_more"
)}
</ha-button>
</ha-row-item>
`
: encrypted
? html`
<ha-md-list-item>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted"
@@ -154,82 +179,54 @@ class HaConfigBackupDetails extends LitElement {
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_description"
`ui.panel.config.backup.location.encryption.location_encrypted_description`
)}
</span>
<ha-button
href="https://www.nabucasa.com/config/backups/"
target="_blank"
slot="end"
rel="noreferrer noopener"
appearance="plain"
size="small"
@click=${this._turnOffEncryption}
variant="danger"
>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_learn_more"
"ui.panel.config.backup.location.encryption.encryption_turn_off"
)}
</ha-button>
</ha-md-list-item>
</ha-row-item>
`
: encrypted
? html`
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_encrypted"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.location.encryption.location_encrypted_description`
)}
</span>
<ha-button
slot="end"
@click=${this._turnOffEncryption}
variant="danger"
>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.encryption_turn_off"
)}
</ha-button>
</ha-md-list-item>
`
: html`
<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off"
: html`
<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off"
)}
>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off_description"
)}
</ha-alert>
<ha-row-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_unencrypted"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.location.encryption.location_unencrypted_description`
)}
</span>
<ha-button
slot="end"
@click=${this._turnOnEncryption}
>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off_description"
"ui.panel.config.backup.location.encryption.encryption_turn_on"
)}
</ha-alert>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.location.encryption.location_unencrypted"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.location.encryption.location_unencrypted_description`
)}
</span>
<ha-button
slot="end"
@click=${this._turnOnEncryption}
>
${this.hass.localize(
"ui.panel.config.backup.location.encryption.encryption_turn_on"
)}
</ha-button>
</ha-md-list-item>
`}
</ha-md-list>
</ha-button>
</ha-row-item>
`}
</div>
</ha-card>
`}
@@ -336,29 +333,16 @@ class HaConfigBackupDetails extends LitElement {
display: flex;
justify-content: flex-end;
}
ha-md-list {
background: none;
padding: 0;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-list-item-two-line-container-height: 64px;
}
ha-md-list-item img {
ha-row-item img {
width: 48px;
}
ha-md-list-item ha-svg-icon[slot="start"] {
ha-row-item ha-svg-icon[slot="start"] {
--mdc-icon-size: 48px;
color: var(--primary-text-color);
}
ha-md-list.summary ha-md-list-item {
--md-list-item-supporting-text-size: 1rem;
--md-list-item-label-text-size: 0.875rem;
--md-list-item-label-text-color: var(--secondary-text-color);
--md-list-item-supporting-text-color: var(--primary-text-color);
}
.warning {
color: var(--error-color);
}
@@ -371,12 +355,8 @@ class HaConfigBackupDetails extends LitElement {
ha-backup-data-picker {
display: block;
}
ha-md-list-item [slot="supporting-text"] {
display: flex;
align-items: center;
flex-direction: row;
gap: var(--ha-space-2);
line-height: var(--ha-line-height-condensed);
ha-row-item::part(supporting-text) {
white-space: wrap;
}
.dot {
display: block;
@@ -6,8 +6,8 @@ import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-switch";
import "../../../../components/item/ha-row-item";
import { formatDate } from "../../../../common/datetime/format_date";
import type { HaSwitch } from "../../../../components/ha-switch";
@@ -143,7 +143,7 @@ export class CloudRemotePref extends LitElement {
"ui.panel.config.cloud.account.remote.security_options"
)}
>
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.external_activation"
@@ -160,9 +160,9 @@ export class CloudRemotePref extends LitElement {
@change=${this._toggleAllowRemoteEnabledChanged}
>
</ha-switch>
</ha-md-list-item>
</ha-row-item>
<hr />
<ha-md-list-item>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.certificate_info"
@@ -194,7 +194,7 @@ export class CloudRemotePref extends LitElement {
"ui.panel.config.cloud.account.remote.more_info"
)}
</ha-button>
</ha-md-list-item>
</ha-row-item>
</ha-expansion-panel>
</div>
</ha-card>
@@ -281,10 +281,12 @@ export class CloudRemotePref extends LitElement {
ha-expansion-panel {
margin-top: 16px;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
ha-row-item::part(headline),
ha-row-item::part(supporting-text) {
white-space: wrap;
}
ha-expansion-panel {
--expansion-panel-content-padding: 0 16px;
@@ -4,9 +4,9 @@ import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-spinner";
import "../../../../components/ha-switch";
import "../../../../components/item/ha-row-item";
import type { CloudStatusLoggedIn, CloudWebhook } from "../../../../data/cloud";
import { createCloudhook, deleteCloudhook } from "../../../../data/cloud";
import type { Webhook, WebhookError } from "../../../../data/webhook";
@@ -76,7 +76,7 @@ export class CloudWebhooks extends LitElement {
`
: this._localHooks.map(
(entry) => html`
<ha-md-list-item .entry=${entry}>
<ha-row-item .entry=${entry}>
<span slot="headline"
>${entry.name}
${entry.domain !== entry.name.toLowerCase()
@@ -108,7 +108,7 @@ export class CloudWebhooks extends LitElement {
@click=${this._enableWebhook}
>
</ha-switch>`}
</ha-md-list-item>
</ha-row-item>
`
)}
<div class="footer">
@@ -237,12 +237,12 @@ export class CloudWebhooks extends LitElement {
.footer a {
color: var(--primary-color);
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
ha-row-item {
--ha-row-item-padding-inline: 0;
}
ha-md-list-item [slot="supporting-text"] {
ha-row-item::part(headline),
ha-row-item::part(supporting-text) {
white-space: wrap;
word-break: break-all;
}
`,
@@ -1,24 +1,24 @@
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-card";
import "../../../../components/ha-button";
import "../../../../components/ha-md-list";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/entity/ha-entity-picker";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "./ha-debug-connection-row";
import "./ha-debug-disable-view-transition-row";
import "./ha-debug-viewport-environment-card";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/list/ha-list-base";
import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import { getExtendedEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import {
getStatisticMetadata,
validateStatistics,
} from "../../../../data/recorder";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { getExtendedEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import "./ha-debug-connection-row";
import "./ha-debug-disable-view-transition-row";
import "./ha-debug-viewport-environment-card";
@customElement("developer-tools-debug")
class HaPanelDevDebug extends SubscribeMixin(LitElement) {
@@ -34,14 +34,14 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
"ui.panel.config.developer-tools.tabs.debug.title"
)}
>
<ha-md-list>
<ha-list-base>
<ha-debug-connection-row
.hass=${this.hass}
></ha-debug-connection-row>
<ha-debug-disable-view-transition-row
.hass=${this.hass}
></ha-debug-disable-view-transition-row>
</ha-md-list>
</ha-list-base>
</ha-card>
<ha-card
.header=${this.hass.localize(
@@ -131,11 +131,6 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
max-width: 600px;
margin: 0 auto;
}
ha-md-list {
padding-top: 0;
padding-bottom: 0;
background: none;
}
`,
];
}
@@ -1,7 +1,7 @@
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-md-list-item";
import "../../../../components/item/ha-list-item-base";
import "../../../../components/ha-switch";
import type { HaSwitch } from "../../../../components/ha-switch";
import type { HomeAssistant } from "../../../../types";
@@ -14,7 +14,7 @@ class HaDebugConnectionRow extends LitElement {
protected render(): TemplateResult {
return html`
<ha-md-list-item>
<ha-list-item-base>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.debug_connection.title"
@@ -30,7 +30,7 @@ class HaDebugConnectionRow extends LitElement {
.checked=${this.hass.debugConnection}
@change=${this._checkedChanged}
></ha-switch>
</ha-md-list-item>
</ha-list-item-base>
`;
}
@@ -3,9 +3,9 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { storage } from "../../../../common/decorators/storage";
import { setViewTransitionDisabled } from "../../../../common/util/view-transition";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-switch";
import type { HaSwitch } from "../../../../components/ha-switch";
import "../../../../components/item/ha-list-item-base";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-debug-disable-view-transition-row")
@@ -17,7 +17,7 @@ class HaDebugDisableViewTransitionRow extends LitElement {
protected render(): TemplateResult {
return html`
<ha-md-list-item>
<ha-list-item-base>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.title"
@@ -33,7 +33,7 @@ class HaDebugDisableViewTransitionRow extends LitElement {
.checked=${this._disabled}
@change=${this._checkedChanged}
></ha-switch>
</ha-md-list-item>
</ha-list-item-base>
`;
}
+236 -160
View File
@@ -7,10 +7,14 @@ import {
mdiDownload,
mdiMicrophone,
mdiOpenInNew,
mdiPalette,
mdiPencil,
mdiPlus,
mdiRestore,
mdiRobot,
mdiScriptText,
mdiShapeOutline,
mdiTools,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
@@ -29,6 +33,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { stringCompare } from "../../../common/string/compare";
import { slugify } from "../../../common/string/slugify";
import { computeRTL } from "../../../common/util/compute_rtl";
@@ -41,8 +46,9 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-list";
import "../../../components/ha-list-item";
import "../../../components/item/ha-list-item-base";
import "../../../components/item/ha-list-item-button";
import "../../../components/list/ha-list-nav";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
@@ -89,6 +95,7 @@ import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { isHelperDomain } from "../helpers/const";
import { brandsUrl } from "../../../util/brands-url";
import { fileDownload } from "../../../util/file_download";
import "../../logbook/ha-logbook";
@@ -101,6 +108,51 @@ import {
showDeviceRegistryDetailDialog,
} from "./device-registry-detail/show-dialog-device-registry-detail";
type DeviceQuickLinkKey =
| "entities"
| "helpers"
| "automations"
| "scenes"
| "scripts";
const NAVIGATION_ACTIONS: {
value: string;
path: string;
icon: string;
countKey: DeviceQuickLinkKey;
}[] = [
{
value: "navigate-entities",
path: "/config/entities",
icon: mdiShapeOutline,
countKey: "entities",
},
{
value: "navigate-helpers",
path: "/config/helpers",
icon: mdiTools,
countKey: "helpers",
},
{
value: "navigate-automations",
path: "/config/automation/dashboard",
icon: mdiRobot,
countKey: "automations",
},
{
value: "navigate-scenes",
path: "/config/scene/dashboard",
icon: mdiPalette,
countKey: "scenes",
},
{
value: "navigate-scripts",
path: "/config/script/dashboard",
icon: mdiScriptText,
countKey: "scripts",
},
] as const;
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null;
}
@@ -224,6 +276,18 @@ export class HaConfigDevicePage extends LitElement {
),
}));
private _getQuickLinkCounts = memoizeOne(
(entities: EntityRegistryEntry[], related?: RelatedResult) => ({
entities: entities.length,
helpers: entities.filter((entity) =>
isHelperDomain(computeDomain(entity.entity_id))
).length,
automations: related?.automation?.length ?? 0,
scenes: related?.scene?.length ?? 0,
scripts: related?.script?.length ?? 0,
})
);
private _deviceIdInList = memoizeOne((deviceId: string) => [deviceId]);
private _entityIds = memoizeOne(
@@ -363,6 +427,7 @@ export class HaConfigDevicePage extends LitElement {
this.hass.devices
);
const entitiesByCategory = this._entitiesByCategory(entities);
const quickLinkCounts = this._getQuickLinkCounts(entities, this._related);
const batteryEntity = this._batteryEntity(entities);
const batteryChargingEntity = this._batteryChargingEntity(entities);
const battery = batteryEntity
@@ -375,35 +440,42 @@ export class HaConfigDevicePage extends LitElement {
: undefined;
const area = device.area_id ? this.hass.areas[device.area_id] : undefined;
const deviceInfo: TemplateResult[] = integrations.map(
(integration) =>
html`<a
slot="actions"
href=${`/config/integrations/integration/${integration.domain}#config_entry=${integration.entry_id}`}
>
<ha-list-item graphic="icon" hasMeta>
<img
slot="graphic"
alt=${domainToName(this.hass.localize, integration.domain)}
src=${brandsUrl(
{
domain: integration.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
${domainToName(this.hass.localize, integration.domain)}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>`
);
const deviceInfo: TemplateResult[] = integrations.length
? [
html`<ha-list-nav slot="actions">
${integrations.map(
(integration) =>
html`<ha-list-item-button
href=${`/config/integrations/integration/${integration.domain}#config_entry=${integration.entry_id}`}
.headline=${domainToName(
this.hass.localize,
integration.domain
)}
>
<img
slot="start"
alt=${domainToName(this.hass.localize, integration.domain)}
src=${brandsUrl(
{
domain: integration.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
width="24"
height="24"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>`
)}
</ha-list-nav>`,
]
: [];
const actions = [...(this._deviceActions || [])];
if (Array.isArray(this._diagnosticDownloadLinks)) {
@@ -502,41 +574,36 @@ export class HaConfigDevicePage extends LitElement {
"ui.panel.config.devices.automation.automations_heading"
)}
</h3>
${this._related.automation?.length
? html`
<div class="items">
${this._getRelated(
this._related
).automation.map((automation) =>
automation
? html`<a
href=${ifDefined(
automation.attributes.id
? `/config/automation/edit/${encodeURIComponent(automation.attributes.id)}`
: `/config/automation/show/${automation.entity_id}`
)}
>
<ha-list-item
hasMeta
.automation=${automation}
>
${computeStateName(automation)}
<ha-icon-next
slot="meta"
></ha-icon-next>
</ha-list-item>
</a>`
: nothing
)}
</div>
`
: html`
<ha-list-item noninteractive>
${this.hass.localize(
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.devices.automation.automations_heading"
)}
>
${this._related.automation?.length
? this._getRelated(
this._related
).automation.map((automation) =>
automation
? html`<ha-list-item-button
.headline=${computeStateName(
automation
)}
.href=${automation.attributes.id
? `/config/automation/edit/${encodeURIComponent(automation.attributes.id)}`
: `/config/automation/show/${automation.entity_id}`}
>
<ha-icon-next
slot="end"
></ha-icon-next>
</ha-list-item-button>`
: nothing
)
: html`<ha-list-item-base
.headline=${this.hass.localize(
"ui.panel.config.devices.automation.no_automations"
)}
</ha-list-item>
`}
></ha-list-item-base>`}
</ha-list-nav>
`
: nothing}
${isComponentLoaded(this.hass.config, "script")
@@ -546,12 +613,14 @@ export class HaConfigDevicePage extends LitElement {
"ui.panel.config.devices.script.scripts_heading"
)}
</h3>
${this._related.script?.length
? html`
<div class="items">
${this._getRelated(
this._related
).script.map((script) => {
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.devices.script.scripts_heading"
)}
>
${this._related.script?.length
? this._getRelated(this._related).script.map(
(script) => {
if (!script) {
return nothing;
}
@@ -562,28 +631,23 @@ export class HaConfigDevicePage extends LitElement {
? `/config/script/edit/${entry.unique_id}`
: `/config/script/show/${script.entity_id}`;
return html`
<a href=${url}>
<ha-list-item
hasMeta
.script=${script}
>
${computeStateName(script)}
<ha-icon-next
slot="meta"
></ha-icon-next>
</ha-list-item>
</a>
<ha-list-item-button
.headline=${computeStateName(script)}
.href=${url}
>
<ha-icon-next
slot="end"
></ha-icon-next>
</ha-list-item-button>
`;
})}
</div>
`
: html`
<ha-list-item noninteractive>
${this.hass.localize(
}
)
: html`<ha-list-item-base
.headline=${this.hass.localize(
"ui.panel.config.devices.script.no_scripts"
)}
</ha-list-item>
`}
></ha-list-item-base>`}
</ha-list-nav>
`
: nothing}
${hasSceneSupport
@@ -593,67 +657,69 @@ export class HaConfigDevicePage extends LitElement {
"ui.panel.config.devices.scene.scenes_heading"
)}
</h3>
${this._related.scene?.length
? html`
<div class="items">
${this._getRelated(this._related).scene.map(
(scene) =>
scene?.attributes.id
? html`
<a
href=${`/config/scene/edit/${scene.attributes.id}`}
>
<ha-list-item
hasMeta
.scene=${scene}
>
${computeStateName(scene)}
<ha-icon-next
slot="meta"
></ha-icon-next>
</ha-list-item>
</a>
`
: html`
<ha-list-item
.id="scene-${slugify(
scene.entity_id
)}"
hasMeta
.scene=${scene}
>
${computeStateName(scene)}
<ha-icon-next
slot="meta"
></ha-icon-next>
</ha-list-item>
<ha-tooltip
.for="scene-${slugify(
scene.entity_id
)}"
placement=${computeRTL(
this.hass.language,
this.hass.translationMetadata
.translations
)
? "left"
: "right"}
>
${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
</ha-tooltip>
`
)}
</div>
`
: html`
<ha-list-item noninteractive>
${this.hass.localize(
<ha-list-nav
.ariaLabel=${this.hass.localize(
"ui.panel.config.devices.scene.scenes_heading"
)}
>
${this._related.scene?.length
? this._getRelated(this._related).scene.map(
(scene) => {
if (!scene) {
return nothing;
}
const sceneId = `scene-${slugify(
scene.entity_id
)}`;
return scene.attributes.id
? html`
<ha-list-item-button
.headline=${computeStateName(
scene
)}
.href=${`/config/scene/edit/${scene.attributes.id}`}
>
<ha-icon-next
slot="end"
></ha-icon-next>
</ha-list-item-button>
`
: html`
<ha-list-item-base
id=${sceneId}
.headline=${computeStateName(
scene
)}
>
<ha-icon-next
slot="end"
></ha-icon-next>
</ha-list-item-base>
<ha-tooltip
.for=${sceneId}
placement=${computeRTL(
this.hass.language,
this.hass.translationMetadata
.translations
)
? "left"
: "right"}
>
${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
</ha-tooltip>
`;
}
)
: html`<ha-list-item-base
.headline=${this.hass.localize(
"ui.panel.config.devices.scene.no_scenes"
)}
</ha-list-item>
`}
></ha-list-item-base>`}
</ha-list-nav>
`
: nothing}
`
@@ -705,16 +771,18 @@ export class HaConfigDevicePage extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
<a href=${`/config/entities?historyBack=1&device=${this.deviceId}`}>
<ha-dropdown-item>
<ha-svg-icon .path=${mdiShapeOutline} slot="icon"></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.entities`,
{ count: entities.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
</a>
${NAVIGATION_ACTIONS.map(
(action) => html`
<ha-dropdown-item value=${action.value}>
<ha-svg-icon slot="icon" .path=${action.icon}></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.devices.quick_links.${action.countKey}`,
{ count: quickLinkCounts[action.countKey] }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
`
)}
<wa-divider></wa-divider>
@@ -1361,6 +1429,11 @@ export class HaConfigDevicePage extends LitElement {
private _handleToolbarMenuAction(ev: HaDropdownSelectEvent) {
const action = ev.detail?.item?.value;
const navAction = NAVIGATION_ACTIONS.find((a) => a.value === action);
if (navAction) {
navigate(`${navAction.path}?historyBack=1&device=${this.deviceId}`);
return;
}
if (action === "reset_entity_ids") {
this._resetEntityIds();
}
@@ -1680,8 +1753,11 @@ export class HaConfigDevicePage extends LitElement {
height: 18px;
}
.items {
padding-bottom: var(--ha-space-4);
ha-list-item-base ha-icon-next,
ha-list-item-button ha-icon-next {
color: var(--secondary-text-color);
--mdc-icon-size: 24px;
display: block;
}
ha-card:has(ha-logbook) {
@@ -251,12 +251,13 @@ export class HaConfigDeviceDashboard extends LitElement {
}
private _setFiltersFromUrl() {
const area = this._searchParms.get("area");
const domain = this._searchParms.get("domain");
const configEntry = this._searchParms.get("config_entry");
const subEntry = this._searchParms.get("sub_entry");
const label = this._searchParms.has("label");
if (!domain && !configEntry && !label) {
if (!area && !domain && !configEntry && !label) {
return;
}
@@ -271,6 +272,10 @@ export class HaConfigDeviceDashboard extends LitElement {
],
items: undefined,
},
"ha-filter-floor-areas": {
value: area ? { areas: [area] } : undefined,
items: undefined,
},
"ha-filter-integrations": {
value: domain ? [domain] : [],
items: undefined,
@@ -1092,6 +1092,7 @@ export class HaConfigEntities extends LitElement {
}
private _setFiltersFromUrl() {
const area = this._searchParms.get("area");
const domain = this._searchParms.get("domain");
const configEntry = this._searchParms.get("config_entry");
const subEntry = this._searchParms.get("sub_entry");
@@ -1099,7 +1100,7 @@ export class HaConfigEntities extends LitElement {
const label = this._searchParms.get("label");
const voiceAssistant = this._searchParms.get("voice_assistant");
if (!domain && !configEntry && !label && !device) {
if (!area && !domain && !configEntry && !label && !device) {
return;
}
@@ -1108,6 +1109,7 @@ export class HaConfigEntities extends LitElement {
this._filters = {
"ha-filter-states": [],
"ha-filter-floor-areas": area ? { areas: [area] } : undefined,
"ha-filter-integrations": domain ? [domain] : [],
"ha-filter-devices": device ? [device] : [],
"ha-filter-labels": label ? [label] : [],
@@ -625,7 +625,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.route=${this.route}
.tabs=${configSections.devices}
.searchLabel=${this.hass.localize(
@@ -964,12 +966,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
};
private _setFiltersFromUrl() {
const area = this._searchParms.get("area");
const device = this._searchParms.get("device");
const label = this._searchParms.get("label");
const category = this._searchParms.get("category");
const voiceAssistant = this._searchParms.get("voice_assistant");
if (!category && !label && !device) {
if (!area && !category && !label && !device && !voiceAssistant) {
return;
}
@@ -977,6 +980,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
this._filter = history.state?.filter || "";
this._filters = {
"ha-filter-floor-areas": area ? { areas: [area] } : undefined,
"ha-filter-devices": device ? [device] : [],
"ha-filter-labels": label ? [label] : [],
"ha-filter-categories": category ? [category] : [],
@@ -19,6 +19,7 @@ import type {
HaScannerType,
} from "../../../../../data/bluetooth";
import {
isScannerStateMismatch,
subscribeBluetoothConnectionAllocations,
subscribeBluetoothScannerState,
subscribeBluetoothScannersDetails,
@@ -285,9 +286,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
const scannerType: HaScannerType =
scannerDetails?.scanner_type ?? "unknown";
const isRemoteScanner = scannerType === "remote";
const hasMismatch =
scannerState &&
scannerState.current_mode !== scannerState.requested_mode;
const hasMismatch = scannerState && isScannerStateMismatch(scannerState);
const allocations = scannerDetails
? this._connectionAllocationData.find(
@@ -438,6 +437,13 @@ export class BluetoothAdapterInfoPage extends LitElement {
);
}
if (scannerState.requested_mode === "auto") {
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_auto_with_current",
{ current: this._formatMode(scannerState.current_mode) }
);
}
return this._formatModeLabel(scannerState.current_mode);
}
@@ -23,6 +23,7 @@ import type {
BluetoothScannerState,
} from "../../../../../data/bluetooth";
import {
isScannerStateMismatch,
subscribeBluetoothAdvertisements,
subscribeBluetoothConnectionAllocations,
subscribeBluetoothScannerState,
@@ -144,7 +145,7 @@ export class BluetoothConfigDashboard extends LitElement {
0
);
const hasMismatch = Object.values(this._scannerStates).some(
(s) => s.current_mode !== s.requested_mode
isScannerStateMismatch
);
const isOffline = adapterCount === 0;
const status = isOffline ? "offline" : hasMismatch ? "warning" : "online";
@@ -1,5 +1,5 @@
import type { TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import type { CSSResultGroup, TemplateResult } 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";
@@ -121,6 +121,19 @@ class DialogBluetoothDeviceInfo extends LitElement {
)}
</tbody>
</table>
${this._params.entry.raw
? html`
<h4>
${this.hass.localize(
"ui.panel.config.bluetooth.raw_advertisement"
)}
</h4>
<div class="raw">
${this.showDataAsHex(this._params.entry.raw)}
</div>
`
: nothing}
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
@@ -133,6 +146,14 @@ class DialogBluetoothDeviceInfo extends LitElement {
</ha-dialog>
`;
}
static readonly styles: CSSResultGroup = css`
.raw {
word-break: break-all;
font-family: var(--ha-font-family-code);
font-size: var(--ha-font-size-s);
}
`;
}
declare global {
@@ -0,0 +1,429 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-button";
import "../../../../../components/ha-dialog";
import "../../../../../components/ha-dialog-footer";
import "../../../../../components/ha-icon-button";
import "../../../../../components/input/ha-input-search";
import "../../../../../components/item/ha-list-item-option";
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
import "../../../../../components/list/ha-list-selectable";
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
import type { HaListSelectedDetail } from "../../../../../components/list/types";
import "../../../../../components/ha-spinner";
import type { ZHADeviceEndpoint, ZHAGroup } from "../../../../../data/zha";
import {
addMembersToGroup,
fetchGroup,
fetchGroupableDevices,
} from "../../../../../data/zha";
import type { HassDialog } from "../../../../../dialogs/make-dialog-manager";
import { haStyleScrollbar } from "../../../../../resources/styles";
import { loadVirtualizer } from "../../../../../resources/virtualizer";
import type { HomeAssistant } from "../../../../../types";
import type { ZHAAddGroupMembersDialogParams } from "./show-dialog-zha-add-group-members";
@customElement("dialog-zha-add-group-members")
class DialogZHAAddGroupMembers
extends LitElement
implements HassDialog<ZHAAddGroupMembersDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _deviceEndpoints: ZHADeviceEndpoint[] = [];
@state() private _filter = "";
@state() private _group?: ZHAGroup;
@state() private _loading = false;
@state() private _open = false;
@state() private _params?: ZHAAddGroupMembersDialogParams;
@state() private _processingAdd = false;
@state() private _selectedDevicesToAdd: string[] = [];
@state() private _virtualizerReady = false;
private _fetchDataToken = 0;
public showDialog(params: ZHAAddGroupMembersDialogParams): void {
this._params = params;
this._deviceEndpoints = [];
this._filter = "";
this._group = undefined;
this._selectedDevicesToAdd = [];
this._open = true;
this._fetchData();
}
public closeDialog(): boolean {
if (this._processingAdd) {
return false;
}
this._open = false;
return true;
}
private _dialogClosed(): void {
this._params = undefined;
this._deviceEndpoints = [];
this._filter = "";
this._group = undefined;
this._loading = false;
this._processingAdd = false;
this._selectedDevicesToAdd = [];
this._virtualizerReady = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult | typeof nothing {
if (!this._params) {
return nothing;
}
const deviceEndpoints = this._filteredDeviceEndpoints;
const showSearch =
this._availableDeviceEndpoints.length > 5 || this._filter;
return html`
<ha-dialog
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.zha.groups.add_members"
)}
?prevent-scrim-close=${this._selectedDevicesToAdd.length > 0}
@after-show=${this._loadVirtualizer}
@closed=${this._dialogClosed}
>
<ha-icon-button
slot="headerNavigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
.disabled=${this._processingAdd}
@click=${this.closeDialog}
></ha-icon-button>
<div class="content">
${this._loading
? this._renderLoadingSpinner()
: html`
${showSearch
? html`
<ha-input-search
appearance="outlined"
.value=${this._filter}
@input=${this._handleFilterChanged}
></ha-input-search>
`
: nothing}
<div class="list-container">
${deviceEndpoints.length
? html`
${this._virtualizerReady
? html`
<ha-list-selectable
multi
@ha-list-selected=${this._handleSelected}
>
<lit-virtualizer
scroller
class="ha-scrollbar"
.items=${deviceEndpoints}
.renderItem=${this._renderDeviceEndpoint}
.keyFunction=${this._keyFunction}
></lit-virtualizer>
</ha-list-selectable>
`
: this._renderLoadingSpinner()}
`
: html`
<div class="empty-list">
${this._filter
? this.hass.localize(
"ui.panel.config.zha.groups.no_devices_found"
)
: this.hass.localize(
"ui.panel.config.zha.groups.no_devices_to_add"
)}
</div>
`}
</div>
`}
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
.disabled=${this._processingAdd}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
.disabled=${this._loading ||
!this._selectedDevicesToAdd.length ||
this._processingAdd}
.loading=${this._processingAdd}
@click=${this._addMembersToGroup}
>
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>
`;
}
private _renderLoadingSpinner(): TemplateResult {
return html`
<div class="spinner-container">
<ha-spinner size="medium"></ha-spinner>
</div>
`;
}
private get _availableDeviceEndpoints(): ZHADeviceEndpoint[] {
if (!this._group) {
return [];
}
return this._deviceEndpoints.filter(
(deviceEndpoint) =>
!this._group!.members.some(
(member) =>
member.device.ieee === deviceEndpoint.device.ieee &&
member.endpoint_id === deviceEndpoint.endpoint_id
)
);
}
private get _filteredDeviceEndpoints(): ZHADeviceEndpoint[] {
const normalizedFilter = this._filter.trim().toLowerCase();
const deviceEndpoints = this._availableDeviceEndpoints;
if (!normalizedFilter) {
return deviceEndpoints;
}
return deviceEndpoints.filter((deviceEndpoint) =>
[
this._deviceEndpointName(deviceEndpoint),
this._deviceEndpointDetails(deviceEndpoint),
deviceEndpoint.device.ieee,
deviceEndpoint.device.manufacturer,
deviceEndpoint.device.model,
]
.filter(Boolean)
.some((value) => value!.toLowerCase().includes(normalizedFilter))
);
}
private async _loadVirtualizer(): Promise<void> {
await loadVirtualizer();
this._virtualizerReady = true;
}
private _keyFunction = (deviceEndpoint: unknown): string =>
this._deviceEndpointId(deviceEndpoint as ZHADeviceEndpoint);
private _renderDeviceEndpoint: RenderItemFunction<ZHADeviceEndpoint> = (
deviceEndpoint
) => {
const id = this._deviceEndpointId(deviceEndpoint);
return html`
<ha-list-item-option
appearance="checkbox"
.value=${id}
.selected=${this._selectedDevicesToAdd.includes(id)}
>
<span slot="headline">${this._deviceEndpointName(deviceEndpoint)}</span>
<span slot="supporting-text">
${this._deviceEndpointDetails(deviceEndpoint)}
</span>
</ha-list-item-option>
`;
};
private _deviceEndpointId(deviceEndpoint: ZHADeviceEndpoint): string {
return `${deviceEndpoint.device.ieee}_${deviceEndpoint.endpoint_id}`;
}
private _deviceEndpointName(deviceEndpoint: ZHADeviceEndpoint): string {
return deviceEndpoint.device.user_given_name || deviceEndpoint.device.name;
}
private _deviceEndpointDetails(deviceEndpoint: ZHADeviceEndpoint): string {
const entityNames = deviceEndpoint.entities.map(
(entity) => entity.name || entity.original_name || entity.entity_id
);
const entitySummary = entityNames.length
? entityNames.length > 2
? `${entityNames.slice(0, 2).join(", ")} +${entityNames.length - 2}`
: entityNames.join(", ")
: this.hass.localize("ui.panel.config.zha.groups.no_entities");
return [
deviceEndpoint.device.area_id
? this.hass.areas[deviceEndpoint.device.area_id]?.name
: undefined,
`${this.hass.localize("ui.panel.config.zha.groups.endpoint")} ${
deviceEndpoint.endpoint_id
}`,
entitySummary,
]
.filter(Boolean)
.join(" · ");
}
private async _fetchData(): Promise<void> {
const token = ++this._fetchDataToken;
this._loading = true;
const [group, deviceEndpoints] = await Promise.all([
fetchGroup(this.hass, this._params!.groupId),
fetchGroupableDevices(this.hass),
]);
if (token !== this._fetchDataToken || !this._params) {
return;
}
this._group = group;
this._deviceEndpoints = deviceEndpoints;
this._loading = false;
}
private _handleFilterChanged(ev: Event): void {
this._filter = (ev.currentTarget as HTMLInputElement).value;
}
private _handleSelected(ev: CustomEvent<HaListSelectedDetail>): void {
const list = ev.currentTarget as HaListSelectable;
let selectedDevicesToAdd = this._selectedDevicesToAdd;
ev.detail.diff?.added.forEach((index) => {
const item = list.items[index] as HaListItemOption | undefined;
if (item?.value && !selectedDevicesToAdd.includes(item.value)) {
selectedDevicesToAdd = [...selectedDevicesToAdd, item.value];
}
});
ev.detail.diff?.removed.forEach((index) => {
const item = list.items[index] as HaListItemOption | undefined;
if (item?.value) {
selectedDevicesToAdd = selectedDevicesToAdd.filter(
(selectedDeviceId) => selectedDeviceId !== item.value
);
}
});
this._selectedDevicesToAdd = selectedDevicesToAdd;
}
private async _addMembersToGroup(): Promise<void> {
this._processingAdd = true;
try {
const members = this._selectedDevicesToAdd.map((member) => {
const memberParts = member.split("_");
return { ieee: memberParts[0], endpoint_id: memberParts[1] };
});
const group = await addMembersToGroup(
this.hass,
this._params!.groupId,
members
);
this._params!.devicesAddedCallback(group);
this._processingAdd = false;
this.closeDialog();
} finally {
this._processingAdd = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
css`
ha-dialog {
--dialog-content-padding: 0;
}
.content {
display: flex;
flex-direction: column;
height: min(520px, calc(100vh - 240px));
}
ha-input-search {
display: block;
margin: 0 var(--ha-space-4) var(--ha-space-2);
}
ha-list-selectable {
display: block;
width: 100%;
height: 100%;
}
ha-list-selectable::part(base) {
width: 100%;
height: 100%;
}
.list-container {
flex: 1 1 auto;
width: 100%;
min-height: 0;
overflow: hidden;
}
lit-virtualizer {
display: block;
width: 100%;
height: 100%;
contain: size layout !important;
}
ha-list-item-option {
display: block;
width: 100%;
height: 64px;
box-sizing: border-box;
--ha-row-item-min-height: 64px;
}
.spinner-container {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: center;
min-height: 160px;
}
ha-spinner {
display: block;
}
.empty-list {
padding: var(--ha-space-6);
color: var(--secondary-text-color);
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-zha-add-group-members": DialogZHAAddGroupMembers;
}
}
@@ -0,0 +1,22 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { ZHAGroup } from "../../../../../data/zha";
export interface ZHAAddGroupMembersDialogParams {
groupId: number;
groupName: string;
devicesAddedCallback: (group: ZHAGroup) => void;
}
export const loadZHAAddGroupMembersDialog = () =>
import("./dialog-zha-add-group-members");
export const showZHAAddGroupMembersDialog = (
element: HTMLElement,
params: ZHAAddGroupMembersDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-zha-add-group-members",
dialogImport: loadZHAAddGroupMembersDialog,
dialogParams: params,
});
};
@@ -3,16 +3,18 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import type { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/input/ha-input";
import type { ZHADeviceEndpoint, ZHAGroup } from "../../../../../data/zha";
import { addGroup, fetchGroupableDevices } from "../../../../../data/zha";
import "../../../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
import "../../../../../components/input/ha-input";
import "./zha-device-endpoint-data-table";
import type { ZHADeviceEndpointDataTable } from "./zha-device-endpoint-data-table";
import "./zha-device-endpoint-list";
import type {
DeviceEndpointSelectionChangedEvent,
ZHADeviceEndpointList,
} from "./zha-device-endpoint-list";
@customElement("zha-add-group-page")
export class ZHAAddGroupPage extends LitElement {
@@ -29,8 +31,8 @@ export class ZHAAddGroupPage extends LitElement {
@state() private _groupId?: string;
@query("zha-device-endpoint-data-table", true)
private _zhaDevicesDataTable!: ZHADeviceEndpointDataTable;
@query("zha-device-endpoint-list", true)
private _zhaDeviceEndpointList!: ZHADeviceEndpointList;
private _firstUpdatedCalled = false;
@@ -57,59 +59,67 @@ export class ZHAAddGroupPage extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.zha.groups.create_group")}
back-path="/config/zha/groups"
>
<ha-config-section .isWide=${!this.narrow}>
<p slot="introduction">
${this.hass.localize(
"ui.panel.config.zha.groups.create_group_details"
)}
</p>
<ha-input
type="string"
.value=${this._groupName}
@change=${this._handleNameChange}
.placeholder=${this.hass!.localize(
"ui.panel.config.zha.groups.group_name_placeholder"
)}
></ha-input>
<div class="container">
<ha-card class="details-card">
<div class="card-header">
${this.hass.localize("ui.panel.config.zha.groups.group_info")}
</div>
<div class="card-content">
<ha-input
type="text"
.value=${this._groupName}
@change=${this._handleNameChange}
.placeholder=${this.hass!.localize(
"ui.panel.config.zha.groups.group_name_placeholder"
)}
></ha-input>
<ha-input
type="number"
.value=${this._groupId}
@change=${this._handleGroupIdChange}
.placeholder=${this.hass!.localize(
"ui.panel.config.zha.groups.group_id_placeholder"
)}
></ha-input>
<ha-input
type="number"
.value=${this._groupId}
@change=${this._handleGroupIdChange}
.placeholder=${this.hass!.localize(
"ui.panel.config.zha.groups.group_id_placeholder"
)}
></ha-input>
</div>
</ha-card>
<div class="header">
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
</div>
<section>
<h2>
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
</h2>
<zha-device-endpoint-data-table
.hass=${this.hass}
.deviceEndpoints=${this.deviceEndpoints}
.narrow=${this.narrow}
selectable
@selection-changed=${this._handleAddSelectionChanged}
>
</zha-device-endpoint-data-table>
<div class="buttons">
<ha-button
.disabled=${!this._groupName ||
this._groupName === "" ||
this._processingAdd}
@click=${this._createGroup}
class="button"
.loading=${this._processingAdd}
<zha-device-endpoint-list
scrollable
show-device-link
.deviceEndpoints=${this.deviceEndpoints}
.narrow=${this.narrow}
.emptyText=${this.hass.localize(
"ui.panel.config.zha.groups.no_devices_to_add"
)}
selectable
@selection-changed=${this._handleAddSelectionChanged}
>
${this.hass!.localize(
"ui.panel.config.zha.groups.create"
)}</ha-button
>
</div>
</ha-config-section>
</zha-device-endpoint-list>
<div class="buttons">
<ha-button
.disabled=${!this._groupName ||
this._groupName === "" ||
this._processingAdd}
@click=${this._createGroup}
.loading=${this._processingAdd}
>
${this.hass!.localize(
"ui.panel.config.zha.groups.create"
)}</ha-button
>
</div>
</section>
</div>
</hass-subpage>
`;
}
@@ -119,7 +129,7 @@ export class ZHAAddGroupPage extends LitElement {
}
private _handleAddSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
): void {
this._selectedDevicesToAdd = ev.detail.value;
}
@@ -142,7 +152,7 @@ export class ZHAAddGroupPage extends LitElement {
this._selectedDevicesToAdd = [];
this._processingAdd = false;
this._groupName = "";
this._zhaDevicesDataTable.clearSelection();
this._zhaDeviceEndpointList.clearSelection();
navigate(`/config/zha/group/${group.group_id}`, { replace: true });
}
@@ -157,29 +167,54 @@ export class ZHAAddGroupPage extends LitElement {
static get styles(): CSSResultGroup {
return [
css`
.header {
font-family: var(--ha-font-family-body);
-webkit-font-smoothing: var(--ha-font-smoothing);
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
font-size: var(--ha-font-size-4xl);
font-weight: var(--ha-font-weight-normal);
.container {
box-sizing: border-box;
max-width: 720px;
margin: 0 auto;
padding: var(--ha-space-4) var(--ha-space-4)
calc(var(--ha-space-20) + var(--safe-area-inset-bottom, 0px));
}
.card-header {
padding: var(--ha-space-4) var(--ha-space-4) 0;
font-size: var(--ha-font-size-xl);
font-weight: var(--ha-font-weight-medium);
line-height: var(--ha-line-height-condensed);
opacity: var(--dark-primary-opacity);
}
.button {
float: right;
.card-content {
display: grid;
gap: var(--ha-space-4);
padding: var(--ha-space-4);
}
ha-config-section *:last-child {
padding-bottom: 24px;
section {
margin-top: var(--ha-space-8);
}
h2 {
margin: 0 0 var(--ha-space-3);
font-family: var(--ha-font-family-body);
font-size: var(--ha-font-size-2xl);
font-weight: var(--ha-font-weight-medium);
line-height: var(--ha-line-height-condensed);
}
zha-device-endpoint-list {
display: block;
min-width: 0;
}
.buttons {
align-items: flex-end;
padding: 16px;
display: flex;
justify-content: flex-end;
padding: var(--ha-space-4) 0 0;
}
.buttons .warning {
--mdc-theme-primary: var(--error-color);
@media (max-width: 600px) {
.container {
padding-inline: var(--ha-space-2);
}
}
`,
];
@@ -1,186 +0,0 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
DataTableRowData,
HaDataTable,
} from "../../../../../components/data-table/ha-data-table";
import type {
ZHADeviceEndpoint,
ZHAEntityReference,
} from "../../../../../data/zha";
import type { HomeAssistant } from "../../../../../types";
import { getAreaTableColumn } from "../../../common/data-table-columns";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { AreaRegistryEntry } from "../../../../../data/area/area_registry";
export interface DeviceEndpointRowData extends DataTableRowData {
id: string;
name: string;
area: string | undefined;
model: string;
manufacturer: string;
endpoint_id: number;
entities: ZHAEntityReference[];
}
@customElement("zha-device-endpoint-data-table")
export class ZHADeviceEndpointDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public selectable = false;
@property({ attribute: false })
public deviceEndpoints: ZHADeviceEndpoint[] = [];
@query("ha-data-table", true) private _dataTable!: HaDataTable;
private _deviceEndpoints = memoizeOne(
(
deviceEndpoints: ZHADeviceEndpoint[],
areas: Record<string, AreaRegistryEntry>
) => {
const outputDevices: DeviceEndpointRowData[] = [];
deviceEndpoints.forEach((deviceEndpoint) => {
outputDevices.push({
name:
deviceEndpoint.device.user_given_name || deviceEndpoint.device.name,
area: deviceEndpoint.device.area_id
? areas[deviceEndpoint.device.area_id].name
: undefined,
model: deviceEndpoint.device.model,
manufacturer: deviceEndpoint.device.manufacturer,
id: deviceEndpoint.device.ieee + "_" + deviceEndpoint.endpoint_id,
ieee: deviceEndpoint.device.ieee,
endpoint_id: deviceEndpoint.endpoint_id,
entities: deviceEndpoint.entities,
dev_id: deviceEndpoint.device.device_reg_id,
});
});
return outputDevices;
}
);
private _columns = memoizeOne(
(localize: LocalizeFunc, narrow: boolean): DataTableColumnContainer =>
narrow
? {
name: {
title: localize("ui.panel.config.zha.groups.members"),
sortable: true,
filterable: true,
direction: "asc",
flex: 2,
template: (device) => html`
<a href=${`/config/devices/device/${device.dev_id}`}>
${device.name}
${device.area
? html` <br />
<span
style="font-size: var(--ha-font-size-s);color: var(--ha-color-text-secondary);"
>
${device.area}
</span>`
: nothing}
</a>
`,
},
endpoint_id: {
title: localize("ui.panel.config.zha.groups.endpoint"),
sortable: true,
filterable: true,
},
}
: {
name: {
title: localize("ui.panel.config.zha.groups.members"),
sortable: true,
filterable: true,
direction: "asc",
flex: 2,
template: (device) => html`
<a href=${`/config/devices/device/${device.dev_id}`}>
${device.name}
</a>
`,
},
area: getAreaTableColumn(localize),
endpoint_id: {
title: localize("ui.panel.config.zha.groups.endpoint"),
sortable: true,
filterable: true,
},
entities: {
title: localize("ui.panel.config.zha.groups.associated_entities"),
sortable: false,
filterable: false,
flex: 2,
template: (device) => html`
${device.entities.length
? device.entities.length > 3
? html`${device.entities
.slice(0, 2)
.map(
(entity) =>
html`<div
style="overflow: hidden; text-overflow: ellipsis;"
>
${entity.name || entity.original_name}
</div>`
)}
<div>+${device.entities.length - 2}</div>`
: device.entities.map(
(entity) =>
html`<div
style="overflow: hidden; text-overflow: ellipsis;"
>
${entity.name || entity.original_name}
</div>`
)
: localize(
"ui.panel.config.zha.groups.no_associated_entities"
)}
`,
},
}
);
public clearSelection() {
this._dataTable.clearSelection();
}
protected render(): TemplateResult {
return html`
<ha-data-table
.columns=${this._columns(this.hass.localize, this.narrow)}
.data=${this._deviceEndpoints(this.deviceEndpoints, this.hass.areas)}
.selectable=${this.selectable}
auto-height
.searchLabel=${this.hass.localize("ui.components.data-table.search")}
.noDataText=${this.hass.localize("ui.components.data-table.no-data")}
></ha-data-table>
`;
}
static get styles(): CSSResultGroup {
return [
css`
.table-cell-text {
word-break: break-word;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-device-endpoint-data-table": ZHADeviceEndpointDataTable;
}
}
@@ -0,0 +1,397 @@
import { consume, type ContextType } from "@lit/context";
import { mdiOpenInNew } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-list";
import "../../../../../components/input/ha-input-search";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/item/ha-list-item-option";
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
import "../../../../../components/list/ha-list-selectable";
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
import type { HaListSelectedDetail } from "../../../../../components/list/types";
import {
areasContext,
internationalizationContext,
} from "../../../../../data/context";
import type {
ZHADeviceEndpoint,
ZHAEntityReference,
} from "../../../../../data/zha";
export interface DeviceEndpointRowData {
id: string;
name: string;
area: string | undefined;
model: string;
manufacturer: string;
endpoint_id: number;
entities: ZHAEntityReference[];
ieee: string;
dev_id: string;
}
export interface DeviceEndpointSelectionChangedEvent {
value: string[];
}
@customElement("zha-device-endpoint-list")
export class ZHADeviceEndpointList extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public selectable = false;
@property({ type: Boolean }) public scrollable = false;
@property({ attribute: false }) public emptyText?: string;
@property({ attribute: "show-device-link", type: Boolean })
public showDeviceLink = false;
@property({ attribute: false })
public deviceEndpoints: ZHADeviceEndpoint[] = [];
@state() private _filter = "";
@state() private _selectedDeviceIds: string[] = [];
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: areasContext, subscribe: true })
private _areas!: ContextType<typeof areasContext>;
@query("ha-list-selectable") private _list?: HaListSelectable;
public clearSelection() {
this._selectedDeviceIds = [];
this._list?.clearSelection();
this._fireSelectionChanged();
}
protected render(): TemplateResult {
const allDeviceEndpoints = this._deviceEndpointRows;
const deviceEndpoints = this._filterDeviceEndpoints(allDeviceEndpoints);
const showSearch = allDeviceEndpoints.length > 5 || this._filter;
return html`
<ha-card
class=${`${showSearch ? "searchable" : ""} ${
this.scrollable ? "scrollable" : ""
}`}
>
${showSearch
? html`
<div class="search">
<ha-input-search
appearance="outlined"
.value=${this._filter}
@input=${this._handleFilterChanged}
></ha-input-search>
</div>
`
: ""}
${deviceEndpoints.length
? html`
${this.selectable
? html`
<ha-list-selectable
multi
@ha-list-selected=${this._handleListSelectionChanged}
>
${repeat(
deviceEndpoints,
(deviceEndpoint) => deviceEndpoint.id,
(deviceEndpoint) =>
this._renderSelectableListRow(deviceEndpoint)
)}
</ha-list-selectable>
`
: html`
<ha-list>
${repeat(
deviceEndpoints,
(deviceEndpoint) => deviceEndpoint.id,
(deviceEndpoint) =>
this._renderReadonlyListRow(deviceEndpoint)
)}
</ha-list>
`}
`
: html`
<div class="empty-list">
${this._filter
? this._i18n.localize(
"ui.panel.config.zha.groups.no_devices_found"
)
: this.emptyText ||
this._i18n.localize("ui.components.data-table.no-data")}
</div>
`}
</ha-card>
`;
}
private get _deviceEndpointRows(): DeviceEndpointRowData[] {
return this.deviceEndpoints.map((deviceEndpoint) => ({
name: deviceEndpoint.device.user_given_name || deviceEndpoint.device.name,
area: deviceEndpoint.device.area_id
? this._areas[deviceEndpoint.device.area_id]?.name
: undefined,
model: deviceEndpoint.device.model,
manufacturer: deviceEndpoint.device.manufacturer,
id: `${deviceEndpoint.device.ieee}_${deviceEndpoint.endpoint_id}`,
ieee: deviceEndpoint.device.ieee,
endpoint_id: deviceEndpoint.endpoint_id,
entities: deviceEndpoint.entities,
dev_id: deviceEndpoint.device.device_reg_id,
}));
}
private _renderSelectableListRow(
deviceEndpoint: DeviceEndpointRowData
): TemplateResult {
const selected = this._selectedDeviceIds.includes(deviceEndpoint.id);
return html`
<ha-list-item-option
appearance="checkbox"
class="device-row"
.value=${deviceEndpoint.id}
.selected=${selected}
>
<span slot="headline">${deviceEndpoint.name}</span>
<span slot="supporting-text">
${this._deviceEndpointDetails(deviceEndpoint)}
</span>
${this.showDeviceLink
? html`
<ha-icon-button
slot="end"
.path=${mdiOpenInNew}
.href=${`/config/devices/device/${deviceEndpoint.dev_id}`}
.label=${this._i18n.localize(
"ui.panel.config.zha.groups.open_device"
)}
@click=${this._stopPropagation}
></ha-icon-button>
`
: nothing}
</ha-list-item-option>
`;
}
private _renderReadonlyListRow(
deviceEndpoint: DeviceEndpointRowData
): TemplateResult {
return html`
<ha-list-item-base class="device-row">
<span slot="headline">${deviceEndpoint.name}</span>
<span slot="supporting-text">
${this._deviceEndpointDetails(deviceEndpoint)}
</span>
${this.showDeviceLink
? html`
<ha-icon-button
slot="end"
.path=${mdiOpenInNew}
.href=${`/config/devices/device/${deviceEndpoint.dev_id}`}
.label=${this._i18n.localize(
"ui.panel.config.zha.groups.open_device"
)}
></ha-icon-button>
`
: nothing}
</ha-list-item-base>
`;
}
private _filterDeviceEndpoints(
deviceEndpoints: DeviceEndpointRowData[]
): DeviceEndpointRowData[] {
const normalizedFilter = this._filter.trim().toLowerCase();
if (!normalizedFilter) {
return deviceEndpoints;
}
return deviceEndpoints.filter((deviceEndpoint) =>
[
deviceEndpoint.name,
this._deviceEndpointDetails(deviceEndpoint),
deviceEndpoint.ieee,
deviceEndpoint.manufacturer,
deviceEndpoint.model,
]
.filter(Boolean)
.some((value) => value!.toLowerCase().includes(normalizedFilter))
);
}
private _deviceEndpointDetails(
deviceEndpoint: DeviceEndpointRowData
): string {
const entityNames = deviceEndpoint.entities.map(
(entity) => entity.name || entity.original_name || entity.entity_id
);
const entitySummary = entityNames.length
? entityNames.length > 2
? `${entityNames.slice(0, 2).join(", ")} +${entityNames.length - 2}`
: entityNames.join(", ")
: this._i18n.localize("ui.panel.config.zha.groups.no_entities");
return [
deviceEndpoint.area,
`${this._i18n.localize("ui.panel.config.zha.groups.endpoint")} ${
deviceEndpoint.endpoint_id
}`,
entitySummary,
]
.filter(Boolean)
.join(" · ");
}
private _handleFilterChanged(ev: Event): void {
this._filter = (ev.currentTarget as HTMLInputElement).value;
}
private _handleListSelectionChanged(
ev: CustomEvent<HaListSelectedDetail>
): void {
const list = ev.currentTarget as HaListSelectable;
let selectedDeviceIds = this._selectedDeviceIds;
ev.detail.diff?.added.forEach((index) => {
const item = list.items[index] as HaListItemOption | undefined;
if (item?.value) {
selectedDeviceIds = this._setSelectedDeviceId(
selectedDeviceIds,
item.value,
true
);
}
});
ev.detail.diff?.removed.forEach((index) => {
const item = list.items[index] as HaListItemOption | undefined;
if (item?.value) {
selectedDeviceIds = this._setSelectedDeviceId(
selectedDeviceIds,
item.value,
false
);
}
});
this._selectedDeviceIds = selectedDeviceIds;
this._fireSelectionChanged();
}
private _setSelectedDeviceId(
selectedDeviceIds: string[],
deviceId: string,
selected: boolean
): string[] {
if (selected) {
return selectedDeviceIds.includes(deviceId)
? selectedDeviceIds
: [...selectedDeviceIds, deviceId];
}
return selectedDeviceIds.filter((selectedDeviceId) => {
return selectedDeviceId !== deviceId;
});
}
private _fireSelectionChanged(): void {
this.dispatchEvent(
new CustomEvent<DeviceEndpointSelectionChangedEvent>(
"selection-changed",
{
detail: { value: this._selectedDeviceIds },
bubbles: true,
composed: true,
}
)
);
}
private _stopPropagation(ev: Event): void {
ev.stopPropagation();
}
static get styles(): CSSResultGroup {
return [
css`
ha-card.scrollable {
display: flex;
flex-direction: column;
overflow: hidden;
}
ha-card.searchable.scrollable {
height: min(520px, calc(100vh - 360px));
}
.search {
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
}
ha-list,
ha-list-selectable {
display: block;
width: 100%;
background: none;
padding: 0;
}
ha-list-selectable::part(base) {
width: 100%;
}
ha-card.scrollable ha-list,
ha-card.scrollable ha-list-selectable {
overflow-y: auto;
}
.device-row {
width: 100%;
--ha-row-item-min-height: 64px;
--ha-row-item-gap: var(--ha-space-3);
}
[slot="headline"],
[slot="supporting-text"] {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-list {
padding: var(--ha-space-6);
color: var(--secondary-text-color);
text-align: center;
}
@media (max-width: 600px) {
ha-card.searchable.scrollable {
height: 440px;
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-device-endpoint-list": ZHADeviceEndpointList;
}
}
@@ -1,30 +1,29 @@
import { mdiDelete } from "@mdi/js";
import { mdiDelete, mdiPlus } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import type { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-list";
import "../../../../../components/ha-list-item";
import type { ZHADeviceEndpoint, ZHAGroup } from "../../../../../data/zha";
import "../../../../../components/ha-svg-icon";
import type { ZHAGroup } from "../../../../../data/zha";
import {
addMembersToGroup,
fetchGroup,
fetchGroupableDevices,
removeGroups,
removeMembersFromGroup,
} from "../../../../../data/zha";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
import { formatAsPaddedHex } from "./functions";
import "./zha-device-endpoint-data-table";
import type { ZHADeviceEndpointDataTable } from "./zha-device-endpoint-data-table";
import "./zha-device-endpoint-list";
import type {
DeviceEndpointSelectionChangedEvent,
ZHADeviceEndpointList,
} from "./zha-device-endpoint-list";
import { showZHAAddGroupMembersDialog } from "./show-dialog-zha-add-group-members";
@customElement("zha-group-page")
export class ZHAGroupPage extends LitElement {
@@ -38,25 +37,12 @@ export class ZHAGroupPage extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false })
public deviceEndpoints: ZHADeviceEndpoint[] = [];
@state() private _processingAdd = false;
@state() private _processingRemove = false;
@state()
private _filteredDeviceEndpoints: ZHADeviceEndpoint[] = [];
@state() private _selectedDevicesToAdd: string[] = [];
@state() private _selectedDevicesToRemove: string[] = [];
@query("#addMembers", true)
private _zhaAddMembersDataTable!: ZHADeviceEndpointDataTable;
@query("#removeMembers")
private _zhaRemoveMembersDataTable!: ZHADeviceEndpointDataTable;
private _zhaRemoveMembersList!: ZHADeviceEndpointList;
private _firstUpdatedCalled = false;
@@ -69,12 +55,8 @@ export class ZHAGroupPage extends LitElement {
public disconnectedCallback(): void {
super.disconnectedCallback();
this._processingAdd = false;
this._processingRemove = false;
this._selectedDevicesToRemove = [];
this._selectedDevicesToAdd = [];
this.deviceEndpoints = [];
this._filteredDeviceEndpoints = [];
}
protected firstUpdated(changedProperties: PropertyValues<this>): void {
@@ -102,6 +84,7 @@ export class ZHAGroupPage extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.group.name}
back-path="/config/zha/groups"
>
<ha-icon-button
slot="toolbar-icon"
@@ -109,158 +92,115 @@ export class ZHAGroupPage extends LitElement {
@click=${this._deleteGroup}
.label=${this.hass.localize("ui.panel.config.zha.groups.delete")}
></ha-icon-button>
<ha-config-section .isWide=${this.isWide}>
<div class="header">
${this.hass.localize("ui.panel.config.zha.groups.group_info")}
</div>
<p slot="introduction">
${this.hass.localize("ui.panel.config.zha.groups.group_details")}
</p>
<p><b>Name:</b> ${this.group.name}</p>
<p><b>Group Id:</b> ${formatAsPaddedHex(this.group.group_id)}</p>
<div class="header">
${this.hass.localize("ui.panel.config.zha.groups.members")}
</div>
<div class="container">
<ha-card>
<ha-list>
${this.group.members.length
? this.group.members.map(
(member) =>
html`<a
href="/config/devices/device/${member.device
.device_reg_id}"
>
<ha-list-item
>${member.device.user_given_name ||
member.device.name}</ha-list-item
>
</a>`
)
: html`
<ha-list-item> This group has no members </ha-list-item>
`}
</ha-list>
</ha-card>
${this.group.members.length
? html`
<div class="header">
${this.hass.localize(
"ui.panel.config.zha.groups.remove_members"
)}
</div>
<zha-device-endpoint-data-table
id="removeMembers"
.hass=${this.hass}
.deviceEndpoints=${this.group.members}
.narrow=${this.narrow}
selectable
@selection-changed=${this._handleRemoveSelectionChanged}
<div class="card-header">
${this.hass.localize("ui.panel.config.zha.groups.group_info")}
</div>
<div class="summary-grid">
<div>
<span class="summary-label"
>${this.hass.localize("ui.common.name")}</span
>
</zha-device-endpoint-data-table>
<span class="summary-value">${this.group.name}</span>
</div>
<div>
<span class="summary-label"
>${this.hass.localize(
"ui.panel.config.zha.groups.group_id"
)}</span
>
<span class="summary-value"
>${formatAsPaddedHex(this.group.group_id)}</span
>
</div>
<div>
<span class="summary-label"
>${this.hass.localize(
"ui.panel.config.zha.groups.members"
)}</span
>
<span class="summary-value">${this.group.members.length}</span>
</div>
</div>
</ha-card>
<div class="buttons">
<ha-button
appearance="plain"
size="small"
variant="danger"
.disabled=${!this._selectedDevicesToRemove.length ||
this._processingRemove}
@click=${this._removeMembersFromGroup}
class="button"
.loading=${this._processingRemove}
>
${this.hass!.localize(
"ui.panel.config.zha.groups.remove_members"
)}</ha-button
>
</div>
`
: nothing}
<div class="header">
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
<div class="members-section">
<h2>${this.hass.localize("ui.panel.config.zha.groups.members")}</h2>
${this.group.members.length
? html`
<zha-device-endpoint-list
id="removeMembers"
scrollable
show-device-link
selectable
.deviceEndpoints=${this.group.members}
.narrow=${this.narrow}
.emptyText=${this.hass.localize(
"ui.panel.config.zha.groups.no_members"
)}
@selection-changed=${this._handleRemoveSelectionChanged}
></zha-device-endpoint-list>
`
: html`
<ha-card class="empty-card">
${this.hass.localize(
"ui.panel.config.zha.groups.no_members"
)}
</ha-card>
`}
<div class="buttons">
${this.group.members.length
? html`
<ha-button
appearance="plain"
variant="danger"
.disabled=${!this._selectedDevicesToRemove.length ||
this._processingRemove}
@click=${this._removeMembersFromGroup}
.loading=${this._processingRemove}
>
${this.hass.localize(
"ui.panel.config.zha.groups.remove_members"
)}
</ha-button>
`
: nothing}
<ha-button @click=${this._showAddMembersDialog}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
</ha-button>
</div>
</div>
<zha-device-endpoint-data-table
id="addMembers"
.hass=${this.hass}
.deviceEndpoints=${this._filteredDeviceEndpoints}
.narrow=${this.narrow}
selectable
@selection-changed=${this._handleAddSelectionChanged}
>
</zha-device-endpoint-data-table>
<div class="buttons">
<ha-button
appearance="plain"
size="small"
.disabled=${!this._selectedDevicesToAdd.length ||
this._processingAdd}
@click=${this._addMembersToGroup}
class="button"
.loading=${this._processingAdd}
>
${this.hass!.localize(
"ui.panel.config.zha.groups.add_members"
)}</ha-button
>
</div>
</ha-config-section>
</div>
</hass-subpage>
`;
}
private _showAddMembersDialog(): void {
showZHAAddGroupMembersDialog(this, {
groupId: this.groupId,
groupName: this.group!.name,
devicesAddedCallback: (group) => {
this.group = group;
this._selectedDevicesToRemove = [];
this._zhaRemoveMembersList?.clearSelection();
},
});
}
private async _fetchData() {
if (this.groupId !== null && this.groupId !== undefined) {
this.group = await fetchGroup(this.hass!, this.groupId);
this.group = await fetchGroup(this.hass, this.groupId);
}
this.deviceEndpoints = await fetchGroupableDevices(this.hass!);
// filter the groupable devices so we only show devices that aren't already in the group
this._filterDevices();
}
private _filterDevices() {
// filter the groupable devices so we only show devices that aren't already in the group
this._filteredDeviceEndpoints = this.deviceEndpoints.filter(
(deviceEndpoint) =>
!this.group!.members.some(
(member) =>
member.device.ieee === deviceEndpoint.device.ieee &&
member.endpoint_id === deviceEndpoint.endpoint_id
)
);
}
private _handleAddSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedDevicesToAdd = ev.detail.value;
}
private _handleRemoveSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
): void {
this._selectedDevicesToRemove = ev.detail.value;
}
private async _addMembersToGroup(): Promise<void> {
this._processingAdd = true;
const members = this._selectedDevicesToAdd.map((member) => {
const memberParts = member.split("_");
return { ieee: memberParts[0], endpoint_id: memberParts[1] };
});
this.group = await addMembersToGroup(this.hass, this.groupId, members);
this._filterDevices();
this._selectedDevicesToAdd = [];
this._zhaAddMembersDataTable.clearSelection();
this._processingAdd = false;
}
private async _removeMembersFromGroup(): Promise<void> {
this._processingRemove = true;
const members = this._selectedDevicesToRemove.map((member) => {
@@ -268,9 +208,8 @@ export class ZHAGroupPage extends LitElement {
return { ieee: memberParts[0], endpoint_id: memberParts[1] };
});
this.group = await removeMembersFromGroup(this.hass, this.groupId, members);
this._filterDevices();
this._selectedDevicesToRemove = [];
this._zhaRemoveMembersDataTable.clearSelection();
this._zhaRemoveMembersList.clearSelection();
this._processingRemove = false;
}
@@ -285,30 +224,78 @@ export class ZHAGroupPage extends LitElement {
hass-subpage {
--app-header-text-color: var(--sidebar-icon-color);
}
.header {
font-family: var(--ha-font-family-body);
-webkit-font-smoothing: var(--ha-font-smoothing);
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
font-size: var(--ha-font-size-4xl);
font-weight: var(--ha-font-weight-normal);
.container {
box-sizing: border-box;
max-width: 720px;
margin: 0 auto;
padding: var(--ha-space-4) var(--ha-space-4)
calc(var(--ha-space-20) + var(--safe-area-inset-bottom, 0px));
}
.card-header {
padding: var(--ha-space-4) var(--ha-space-4) 0;
font-size: var(--ha-font-size-xl);
font-weight: var(--ha-font-weight-medium);
line-height: var(--ha-line-height-condensed);
opacity: var(--dark-primary-opacity);
}
.button {
float: right;
.summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--ha-space-4);
padding: var(--ha-space-4);
}
a {
color: var(--primary-color);
text-decoration: none;
.summary-label,
.summary-value {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.summary-label {
color: var(--secondary-text-color);
font-size: var(--ha-font-size-s);
line-height: var(--ha-line-height-condensed);
}
.summary-value {
margin-top: var(--ha-space-1);
font-size: var(--ha-font-size-l);
line-height: var(--ha-line-height-condensed);
}
.members-section {
margin-top: var(--ha-space-6);
}
h2 {
margin: 0 0 var(--ha-space-3);
font-size: var(--ha-font-size-2xl);
font-weight: var(--ha-font-weight-medium);
line-height: var(--ha-line-height-condensed);
}
.buttons {
align-items: flex-end;
padding: 16px;
display: flex;
gap: var(--ha-space-2);
justify-content: flex-end;
padding: var(--ha-space-4) 0 0;
}
.buttons .warning {
--mdc-theme-primary: var(--error-color);
.empty-card {
padding: var(--ha-space-6);
color: var(--secondary-text-color);
text-align: center;
}
@media (max-width: 600px) {
.summary-grid {
grid-template-columns: 1fr;
gap: var(--ha-space-2);
}
}
`,
];
+42 -2
View File
@@ -410,9 +410,15 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
if (!this._searchParms.has("label")) {
if (
!this._searchParms.has("area") &&
!this._searchParms.has("device") &&
!this._searchParms.has("label")
) {
this._filters = this._storageFilters;
}
this._filterArea();
this._filterDevice();
this._filterLabel();
}
}
@@ -454,7 +460,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.route=${this.route}
.tabs=${configSections.automations}
.searchLabel=${this.hass.localize(
@@ -785,6 +793,38 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._applyFilters();
}
private _filterArea() {
const area = this._searchParms.get("area");
if (!area) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-floor-areas": {
value: { areas: [area] },
items: undefined,
},
};
this._applyFilters();
}
private _filterDevice() {
const device = this._searchParms.get("device");
if (!device) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-devices": {
value: [device],
items: undefined,
},
};
this._applyFilters();
}
private _filterLabel() {
const label = this._searchParms.get("label");
if (!label) {
+1 -1
View File
@@ -560,7 +560,7 @@ export class HaScriptEditor extends SubscribeMixin(
if (stateObj?.state !== UNAVAILABLE) {
return;
}
const validation = await validateConfig(this.hass, {
const validation = await validateConfig(this.hass.callWS, {
actions: this.config.sequence,
});
this.validationErrors = (
+45 -2
View File
@@ -431,7 +431,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.route=${this.route}
.tabs=${configSections.automations}
.searchLabel=${this.hass.localize(
@@ -783,10 +785,19 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
const hasUrlFilter =
this._searchParms.has("blueprint") || this._searchParms.has("label");
this._searchParms.has("area") ||
this._searchParms.has("blueprint") ||
this._searchParms.has("device") ||
this._searchParms.has("label");
if (!hasUrlFilter) {
this._filters = this._storageFilters;
}
if (this._searchParms.has("area")) {
this._filterArea();
}
if (this._searchParms.has("device")) {
this._filterDevice();
}
if (this._searchParms.has("blueprint")) {
this._filterBlueprint();
}
@@ -803,6 +814,38 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
}
}
private _filterArea() {
const area = this._searchParms.get("area");
if (!area) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-floor-areas": {
value: { areas: [area] },
items: undefined,
},
};
this._applyFilters();
}
private _filterDevice() {
const device = this._searchParms.get("device");
if (!device) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-devices": {
value: [device],
items: undefined,
},
};
this._applyFilters();
}
private _filterLabel() {
const label = this._searchParms.get("label");
if (!label) {
+2 -2
View File
@@ -4,7 +4,7 @@ import { ifDefined } from "lit/directives/if-defined";
import "../../layouts/hass-error-screen";
import "../../layouts/hass-subpage";
import type { HomeAssistant, PanelInfo } from "../../types";
import { IFRAME_SANDBOX } from "../../util/iframe";
import { IFRAME_SANDBOX_SAME_ORIGIN } from "../../util/iframe";
@customElement("ha-panel-iframe")
class HaPanelIframe extends LitElement {
@@ -41,7 +41,7 @@ class HaPanelIframe extends LitElement {
this.panel.title === null ? undefined : this.panel.title
)}
src=${this.panel.config.url}
.sandbox=${IFRAME_SANDBOX}
.sandbox=${IFRAME_SANDBOX_SAME_ORIGIN}
allow="fullscreen"
></iframe>
</hass-subpage>
@@ -2,7 +2,7 @@ import {
mdiPause,
mdiPlay,
mdiPlayPause,
mdiPower,
mdiPowerStandby,
mdiPowerOff,
mdiPowerOn,
mdiRepeat,
@@ -225,7 +225,7 @@ class HuiMediaPlayerPlaybackCardFeature
supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_OFF)
) {
buttons.push({
icon: assumedState ? mdiPowerOff : mdiPower,
icon: assumedState ? mdiPowerOff : mdiPowerStandby,
action: "turn_off",
});
}
@@ -237,7 +237,7 @@ class HuiMediaPlayerPlaybackCardFeature
supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON)
) {
buttons.push({
icon: assumedState ? mdiPowerOn : mdiPower,
icon: assumedState ? mdiPowerOn : mdiPowerStandby,
action: "turn_on",
});
}
+2 -2
View File
@@ -13,7 +13,7 @@ import type {
LovelaceGridOptions,
} from "../types";
import type { IframeCardConfig } from "./types";
import { IFRAME_SANDBOX } from "../../../util/iframe";
import { IFRAME_SANDBOX_SAME_ORIGIN } from "../../../util/iframe";
@customElement("hui-iframe-card")
export class HuiIframeCard extends LitElement implements LovelaceCard {
@@ -95,7 +95,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
}
const sandbox_params = this._config.disable_sandbox
? undefined
: `${sandbox_user_params} ${IFRAME_SANDBOX}`;
: `${sandbox_user_params} ${IFRAME_SANDBOX_SAME_ORIGIN}`;
return html`
<ha-card
@@ -753,9 +753,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
ha-icon-button[action="media_play"],
ha-icon-button[action="media_play_pause"],
ha-icon-button[action="media_pause"],
ha-icon-button[action="media_stop"],
ha-icon-button[action="turn_on"],
ha-icon-button[action="turn_off"] {
ha-icon-button[action="media_stop"] {
--ha-icon-button-size: 56px;
--mdc-icon-size: 40px;
}
@@ -844,8 +842,10 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
.narrow ha-icon-button[action="media_play"],
.narrow ha-icon-button[action="media_play_pause"],
.narrow ha-icon-button[action="media_pause"],
.narrow ha-icon-button[action="turn_on"] {
.narrow
ha-icon-button[action="media_pause"]
.narrow
ha-icon-button[action="media_stop"] {
--ha-icon-button-size: 50px;
--mdc-icon-size: 36px;
}
@@ -5,7 +5,10 @@ import { customElement, property, state } from "lit/decorators";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { formatNumber } from "../../../common/number/format_number";
import {
formatNumber,
getNumberFormatOptions,
} from "../../../common/number/format_number";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-state-icon";
@@ -226,7 +229,14 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
? ""
: this._value === null
? "?"
: formatNumber(this._value, this.hass.locale)}</span
: formatNumber(
this._value,
this.hass.locale,
getNumberFormatOptions(
undefined,
this.hass.entities[this._config.entity]
)
)}</span
>
<span class="measurement"
>${this._config.unit ||
@@ -229,9 +229,6 @@ export class HuiEntityEditor extends LitElement {
}
static styles = css`
ha-entity-picker {
margin-top: 8px;
}
.entity {
display: flex;
align-items: center;
@@ -253,6 +250,11 @@ export class HuiEntityEditor extends LitElement {
ha-md-list {
gap: 8px;
padding-top: 0;
display: flex;
flex-direction: column;
}
ha-md-list:has(> *) {
margin-bottom: var(--ha-space-2);
}
ha-md-list-item {
border: 1px solid var(--divider-color);
@@ -2,6 +2,7 @@ import { consume } from "@lit/context";
import {
mdiChevronDown,
mdiChevronRight,
mdiChevronLeft,
mdiMagnify,
mdiTextureBox,
} from "@mdi/js";
@@ -16,6 +17,7 @@ import { computeEntityName } from "../../../../common/entity/compute_entity_name
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { computeRTL } from "../../../../common/util/compute_rtl";
import { debounce } from "../../../../common/util/debounce";
import { mainWindow } from "../../../../common/dom/get_main_window";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-combo-box-item";
import "../../../../components/ha-domain-icon";
@@ -294,7 +296,11 @@ export class HuiSuggestionEntityTree extends LitElement {
private _renderChevron(expanded: boolean): TemplateResult {
return html`<ha-svg-icon
class="chevron"
.path=${expanded ? mdiChevronDown : mdiChevronRight}
.path=${expanded
? mdiChevronDown
: mainWindow.document.dir === "rtl"
? mdiChevronLeft
: mdiChevronRight}
></ha-svg-icon>`;
}
@@ -143,6 +143,10 @@ export class HuiViewEditor extends LitElement {
const data = {
...this._config,
type: this._type,
theme:
this._config.theme?.toLowerCase() === "backend-selected"
? undefined
: this._config.theme,
};
if (data.max_columns === undefined && this._type === SECTIONS_VIEW_LAYOUT) {
@@ -2,7 +2,7 @@ import {
mdiPause,
mdiPlay,
mdiPlayPause,
mdiPower,
mdiPowerStandby,
mdiPowerOff,
mdiPowerOn,
mdiSkipNext,
@@ -198,7 +198,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
entityState !== UNAVAILABLE
? html`
<ha-icon-button
.path=${assumedState ? mdiPowerOn : mdiPower}
.path=${assumedState ? mdiPowerOn : mdiPowerStandby}
.label=${this.hass.localize("ui.card.media_player.turn_on")}
@click=${this._turnOn}
></ha-icon-button>
@@ -216,7 +216,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
(stateActive(stateObj) || assumedState)
? html`
<ha-icon-button
.path=${assumedState ? mdiPowerOff : mdiPower}
.path=${assumedState ? mdiPowerOff : mdiPowerStandby}
.label=${this.hass.localize("ui.card.media_player.turn_off")}
@click=${this._turnOff}
></ha-icon-button>
+28 -40
View File
@@ -1,12 +1,11 @@
import type { PropertyValues, TemplateResult } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { normalizeLuminance } from "../../common/color/palette";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import "../../components/ha-select";
import type { HaSelectSelectEvent } from "../../components/ha-select";
import "../../components/ha-settings-row";
import "../../components/ha-theme-picker";
import "../../components/input/ha-input";
import "../../components/radio/ha-radio-group";
import type { HaRadioGroup } from "../../components/radio/ha-radio-group";
@@ -20,11 +19,14 @@ import {
DefaultAccentColor,
DefaultPrimaryColor,
} from "../../resources/theme/color/color.globals";
import type { HomeAssistant, ThemeSettings } from "../../types";
import type {
HomeAssistant,
ThemeSettings,
ValueChangedEvent,
} from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { clearSelectedThemeState } from "../../util/ha-pref-storage";
const USE_DEFAULT_THEME = "__USE_DEFAULT_THEME__";
const HOME_ASSISTANT_THEME = "default";
@customElement("ha-pick-theme-row")
@@ -33,8 +35,6 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public narrow = false;
@state() _themeNames: string[] = [];
@state() private _userTheme?: ThemeSettings | null;
@state() private _migrating = false;
@@ -88,24 +88,17 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.profile.themes.link_promo")}
</a>
</span>
<ha-select
<ha-theme-picker
.hass=${this.hass}
.label=${this.hass.localize("ui.panel.profile.themes.dropdown_label")}
.noThemeLabel=${this.hass.localize(
"ui.panel.profile.themes.use_default"
)}
.value=${this.hass.selectedTheme?.theme || undefined}
.disabled=${!hasThemes}
.value=${this.hass.selectedTheme?.theme || USE_DEFAULT_THEME}
@selected=${this._handleThemeSelection}
.options=${[
{
value: USE_DEFAULT_THEME,
label: this.hass.localize("ui.panel.profile.themes.use_default"),
},
{ value: HOME_ASSISTANT_THEME, label: "Home Assistant" },
...this._themeNames.map((theme) => ({
value: theme,
label: theme,
})),
]}
>
</ha-select>
include-default
@value-changed=${this._handleThemeSelection}
></ha-theme-picker>
</ha-settings-row>
${curTheme === HOME_ASSISTANT_THEME ||
(curThemeIsUseDefault &&
@@ -194,17 +187,6 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
`;
}
public willUpdate(changedProperties: PropertyValues<this>) {
const oldHass = changedProperties.get("hass") as undefined | HomeAssistant;
const themesChanged =
changedProperties.has("hass") &&
(!oldHass || oldHass.themes.themes !== this.hass.themes.themes);
if (themesChanged) {
this._themeNames = Object.keys(this.hass.themes.themes).sort();
}
}
private _handleColorChange(ev: CustomEvent) {
const target = ev.target as any;
@@ -245,13 +227,14 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
fireEvent(this, "settheme", { dark });
}
private _handleThemeSelection(ev: HaSelectSelectEvent) {
private _handleThemeSelection(
ev: ValueChangedEvent<string | undefined>
): void {
ev.stopPropagation();
const theme = ev.detail.value;
if (theme === this.hass.selectedTheme?.theme) {
return;
}
if (theme === USE_DEFAULT_THEME) {
if (theme === undefined) {
// undefined = "use default"
if (this.hass.selectedTheme?.theme) {
fireEvent(this, "settheme", {
theme: "",
@@ -261,6 +244,11 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
}
return;
}
if (theme === this.hass.selectedTheme?.theme) {
return;
}
fireEvent(this, "settheme", {
theme,
primaryColor: undefined,
@@ -320,7 +308,7 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
margin: 0 4px;
}
ha-select {
ha-theme-picker {
display: block;
width: 100%;
}
+13 -4
View File
@@ -313,8 +313,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
});
clearInterval(this.__backendPingInterval);
// Fetch the brands access token on initial connect and schedule refresh
fetchAndScheduleBrandsAccessToken(this.hass!);
this._refreshBrandsAccessToken();
this.__backendPingInterval = setInterval(() => {
if (this.hass?.connected) {
@@ -340,8 +339,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
this._updateHass({ connected: true });
broadcastConnectionStatus("connected");
// Refresh the brands access token on reconnect and restart refresh schedule
fetchAndScheduleBrandsAccessToken(this.hass!);
this._refreshBrandsAccessToken();
// on reconnect always fetch config as we might miss an update while we were disconnected
// @ts-ignore
@@ -362,4 +360,15 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
clearInterval(this.__backendPingInterval);
clearBrandsTokenRefresh();
}
private async _refreshBrandsAccessToken() {
// The brands WS handler may not be registered yet after a server restart;
// fetchAndScheduleBrandsAccessToken retries internally. If the token
// changed, re-render so any brand <img> elements that rendered against a
// different (or missing) token recompute their src and re-fetch.
const changed = await fetchAndScheduleBrandsAccessToken(this.hass!);
if (changed) {
this._updateHass({});
}
}
};

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