Compare commits

..

6 Commits

Author SHA1 Message Date
Simon Lamon
b8110d1a45 Merge branch 'dev' into sec_pypi_publishing 2025-10-27 06:41:51 +01:00
Simon Lamon
19e9de39c5 Merge branch 'dev' into sec_pypi_publishing 2025-10-19 10:56:12 +02:00
Simon Lamon
f22f01e513 Merge branch 'dev' into sec_pypi_publishing 2025-10-06 20:28:38 +02:00
Simon Lamon
3f86f144b5 Merge branch 'dev' into sec_pypi_publishing 2025-10-04 17:25:20 +02:00
Simon Lamon
4efef5ed16 Update release.yaml 2025-09-24 07:04:06 +02:00
Simon Lamon
cac7ae2a40 Remove twine and introduce trusted publishing 2025-09-20 21:23:04 +02:00
205 changed files with 2637 additions and 4812 deletions

View File

@@ -89,7 +89,7 @@ jobs:
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: frontend-bundle-stats name: frontend-bundle-stats
path: build/stats/*.json path: build/stats/*.json
@@ -113,7 +113,7 @@ jobs:
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: supervisor-bundle-stats name: supervisor-bundle-stats
path: build/stats/*.json path: build/stats/*.json

View File

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

View File

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

View File

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

View File

@@ -19,8 +19,11 @@ jobs:
release: release:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: pypi
permissions: permissions:
contents: write # Required to upload release assets contents: write # Required to upload release assets
id-token: write # For "Trusted Publisher" to PyPi
if: github.repository_owner == 'home-assistant'
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -46,16 +49,20 @@ jobs:
run: ./script/translations_download run: ./script/translations_download
env: env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package - name: Build and release package
run: | run: |
python3 -m pip install twine build python3 -m pip install build
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1 export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/release script/release
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
skip-existing: true
- name: Upload release assets - name: Upload release assets
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with: with:
files: | files: |
dist/*.whl dist/*.whl
@@ -108,7 +115,7 @@ jobs:
- name: Tar folder - name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist . run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset - name: Upload release asset
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with: with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@@ -137,6 +144,6 @@ jobs:
- name: Tar folder - name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build . run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset - name: Upload release asset
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with: with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

2
.nvmrc
View File

@@ -1 +1 @@
22.21.1 22.21.0

View File

@@ -18,16 +18,16 @@ module.exports.sourceMapURL = () => {
module.exports.ignorePackages = () => []; module.exports.ignorePackages = () => [];
// Files from NPM packages that we should replace with empty file // Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) => module.exports.emptyPackages = ({ isHassioBuild }) =>
[ [
require.resolve("@vaadin/vaadin-material-styles/typography.js"), require.resolve("@vaadin/vaadin-material-styles/typography.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"), require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Icons in supervisor conflict with icons in HA so we don't load. // Icons in supervisor conflict with icons in HA so we don't load.
(isHassioBuild || isLandingPageBuild) && isHassioBuild &&
require.resolve( require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon.ts") path.resolve(paths.root_dir, "src/components/ha-icon.ts")
), ),
(isHassioBuild || isLandingPageBuild) && isHassioBuild &&
require.resolve( require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts") path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
), ),
@@ -337,7 +337,6 @@ module.exports.config = {
publicPath: publicPath(latestBuild), publicPath: publicPath(latestBuild),
isProdBuild, isProdBuild,
latestBuild, latestBuild,
isLandingPageBuild: true,
}; };
}, },
}; };

View File

@@ -41,7 +41,6 @@ const createRspackConfig = ({
isStatsBuild, isStatsBuild,
isTestBuild, isTestBuild,
isHassioBuild, isHassioBuild,
isLandingPageBuild,
dontHash, dontHash,
}) => { }) => {
if (!dontHash) { if (!dontHash) {
@@ -169,9 +168,7 @@ const createRspackConfig = ({
}, },
}), }),
new rspack.NormalModuleReplacementPlugin( new rspack.NormalModuleReplacementPlugin(
new RegExp( new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
),
path.resolve(paths.root_dir, "src/util/empty.js") path.resolve(paths.root_dir, "src/util/empty.js")
), ),
!isProdBuild && new LogStartCompilePlugin(), !isProdBuild && new LogStartCompilePlugin(),

View File

@@ -16,9 +16,9 @@ import {
} from "../../../../src/common/auth/token_storage"; } from "../../../../src/common/auth/token_storage";
import { atLeastVersion } from "../../../../src/common/config/version"; import { atLeastVersion } from "../../../../src/common/config/version";
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-icon"; import "../../../../src/components/ha-icon";
import "../../../../src/components/ha-list"; import "../../../../src/components/ha-list";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-list-item"; import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { import {
@@ -28,6 +28,7 @@ import {
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types"; import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view"; import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
import "../../../../src/layouts/hass-loading-screen"; import "../../../../src/layouts/hass-loading-screen";
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
import "./hc-layout"; import "./hc-layout";
@customElement("hc-cast") @customElement("hc-cast")
@@ -95,9 +96,7 @@ class HcCast extends LitElement {
<ha-list @action=${this._handlePickView} activatable> <ha-list @action=${this._handlePickView} activatable>
${( ${(
this.lovelaceViews ?? [ this.lovelaceViews ?? [
{ generateDefaultViewConfig({}, {}, {}, {}, () => ""),
title: "Home",
},
] ]
).map( ).map(
(view, idx) => html` (view, idx) => html`

View File

@@ -5,14 +5,14 @@ subtitle: Dialogs provide important prompts in a user flow.
# Material Design 3 # Material Design 3
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guidelines. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview). Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guideliness. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
# Guidelines # Guidelines
## Design ## Design
- Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead. - Dialogs have a max width of 560px. Alert and confirmation dialogs got a fixed width of 320px. If you need more width, consider a dedicated page instead.
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines. - The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guideliness.
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake. - Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu. - Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom. - The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
@@ -26,7 +26,7 @@ Our dialogs are based on the latest version of Material Design. Please note that
- A best practice is to always use a title, even if it is optional by Material guidelines. - A best practice is to always use a title, even if it is optional by Material guidelines.
- People mainly read the title and a button. Put the most important information in those two. - People mainly read the title and a button. Put the most important information in those two.
- Try to avoid user generated content in the title, this could make the title unreadably long. - Try to avoid user generated content in the title, this could make the title unreadable long.
- If users become unsure, they read the description. Make sure this explains what will happen. - If users become unsure, they read the description. Make sure this explains what will happen.
- Strive for minimalism. - Strive for minimalism.

View File

@@ -39,7 +39,6 @@ const SENSOR_DEVICE_CLASSES = [
"pm1", "pm1",
"pm10", "pm10",
"pm25", "pm25",
"pm4",
"power_factor", "power_factor",
"power", "power",
"precipitation", "precipitation",

View File

@@ -1,25 +1,22 @@
import "@material/mwc-linear-progress"; import "@material/mwc-linear-progress";
import { mdiOpenInNew } from "@mdi/js"; import { type PropertyValues, css, html, nothing } from "lit";
import { css, html, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { extractSearchParam } from "../../src/common/url/search-params";
import "../../src/components/ha-alert"; import "../../src/components/ha-alert";
import "../../src/components/ha-button";
import "../../src/components/ha-fade-in"; import "../../src/components/ha-fade-in";
import "../../src/components/ha-spinner"; import "../../src/components/ha-spinner";
import "../../src/components/ha-svg-icon";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/onboarding/onboarding-welcome-links";
import { onBoardingStyles } from "../../src/onboarding/styles";
import { haStyle } from "../../src/resources/styles"; import { haStyle } from "../../src/resources/styles";
import "./components/landing-page-logs"; import "../../src/onboarding/onboarding-welcome-links";
import "./components/landing-page-network"; import "./components/landing-page-network";
import "./components/landing-page-logs";
import { extractSearchParam } from "../../src/common/url/search-params";
import { onBoardingStyles } from "../../src/onboarding/styles";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { LandingPageBaseElement } from "./landing-page-base-element";
import { import {
getSupervisorNetworkInfo, getSupervisorNetworkInfo,
pingSupervisor, pingSupervisor,
type NetworkInfo, type NetworkInfo,
} from "./data/supervisor"; } from "./data/supervisor";
import { LandingPageBaseElement } from "./landing-page-base-element";
export const ASSUME_CORE_START_SECONDS = 60; export const ASSUME_CORE_START_SECONDS = 60;
const SCHEDULE_CORE_CHECK_SECONDS = 1; const SCHEDULE_CORE_CHECK_SECONDS = 1;
@@ -97,21 +94,16 @@ class HaLandingPage extends LandingPageBaseElement {
<ha-language-picker <ha-language-picker
.value=${this.language} .value=${this.language}
.label=${""} .label=${""}
button-style
native-name native-name
@value-changed=${this._languageChanged} @value-changed=${this._languageChanged}
inline-arrow inline-arrow
></ha-language-picker> ></ha-language-picker>
<ha-button <a
appearance="plain"
variant="neutral"
href="https://www.home-assistant.io/getting-started/onboarding/" href="https://www.home-assistant.io/getting-started/onboarding/"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
>${this.localize("ui.panel.page-onboarding.help")}</a
> >
${this.localize("ui.panel.page-onboarding.help")}
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-button>
</div> </div>
`; `;
} }
@@ -226,8 +218,26 @@ class HaLandingPage extends LandingPageBaseElement {
ha-alert p { ha-alert p {
text-align: unset; text-align: unset;
} }
.footer ha-svg-icon { ha-language-picker {
--mdc-icon-size: var(--ha-space-5); display: block;
width: 200px;
border-radius: var(--ha-border-radius-sm);
overflow: hidden;
--ha-select-height: 40px;
--mdc-select-fill-color: none;
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
--mdc-select-ink-color: var(--primary-text-color, #212121);
--mdc-select-idle-line-color: transparent;
--mdc-select-hover-line-color: transparent;
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
--mdc-shape-small: 0;
}
a {
text-decoration: none;
color: var(--primary-text-color);
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
} }
ha-fade-in { ha-fade-in {
min-height: calc(100vh - 64px - 88px); min-height: calc(100vh - 64px - 88px);

View File

@@ -52,8 +52,8 @@
"@fullcalendar/list": "6.1.19", "@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19", "@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19", "@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-beta.6.ha.7", "@home-assistant/webawesome": "3.0.0-beta.6.ha.6",
"@lezer/highlight": "1.2.3", "@lezer/highlight": "1.2.2",
"@lit-labs/motion": "1.0.9", "@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6", "@lit-labs/observers": "2.0.6",
"@lit-labs/virtualizer": "2.1.1", "@lit-labs/virtualizer": "2.1.1",
@@ -81,7 +81,7 @@
"@material/mwc-top-app-bar": "0.27.0", "@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0", "@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "2.4.1", "@material/web": "2.4.0",
"@mdi/js": "7.4.47", "@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47", "@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3", "@replit/codemirror-indentation-markers": "6.5.3",
@@ -89,8 +89,8 @@
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1", "@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0", "@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.9.4", "@vaadin/combo-box": "24.9.2",
"@vaadin/vaadin-themable-mixin": "24.9.4", "@vaadin/vaadin-themable-mixin": "24.9.2",
"@vibrant/color": "4.0.0", "@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0", "@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10", "@webcomponents/scoped-custom-element-registry": "0.0.10",
@@ -111,7 +111,7 @@
"fuse.js": "7.1.0", "fuse.js": "7.1.0",
"google-timezones-json": "1.2.0", "google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2", "gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.14", "hls.js": "1.6.13",
"home-assistant-js-websocket": "9.5.0", "home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2", "idb-keyval": "6.2.2",
"intl-messageformat": "10.7.18", "intl-messageformat": "10.7.18",
@@ -122,7 +122,7 @@
"lit": "3.3.1", "lit": "3.3.1",
"lit-html": "3.3.1", "lit-html": "3.3.1",
"luxon": "3.7.2", "luxon": "3.7.2",
"marked": "16.4.2", "marked": "16.4.1",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
"node-vibrant": "4.0.3", "node-vibrant": "4.0.3",
"object-hash": "3.0.0", "object-hash": "3.0.0",
@@ -148,17 +148,17 @@
"xss": "1.0.15" "xss": "1.0.15"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.5", "@babel/core": "7.28.4",
"@babel/helper-define-polyfill-provider": "0.6.5", "@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.5", "@babel/plugin-transform-runtime": "7.28.3",
"@babel/preset-env": "7.28.5", "@babel/preset-env": "7.28.3",
"@bundle-stats/plugin-webpack-filter": "4.21.6", "@bundle-stats/plugin-webpack-filter": "4.21.5",
"@lokalise/node-api": "15.3.1", "@lokalise/node-api": "15.3.1",
"@octokit/auth-oauth-device": "8.0.3", "@octokit/auth-oauth-device": "8.0.2",
"@octokit/plugin-retry": "8.0.3", "@octokit/plugin-retry": "8.0.2",
"@octokit/rest": "22.0.1", "@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.3.8", "@rsdoctor/rspack-plugin": "1.3.4",
"@rspack/core": "1.6.1", "@rspack/core": "1.5.8",
"@rspack/dev-server": "1.1.4", "@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22", "@types/chromecast-caf-receiver": "6.0.22",
@@ -173,17 +173,17 @@
"@types/lodash.merge": "4.6.9", "@types/lodash.merge": "4.6.9",
"@types/luxon": "3.7.1", "@types/luxon": "3.7.1",
"@types/mocha": "10.0.10", "@types/mocha": "10.0.10",
"@types/qrcode": "1.5.6", "@types/qrcode": "1.5.5",
"@types/sortablejs": "1.15.9", "@types/sortablejs": "1.15.8",
"@types/tar": "6.1.13", "@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.7", "@vitest/coverage-v8": "4.0.1",
"babel-loader": "10.0.0", "babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3", "browserslist-useragent-regexp": "4.1.3",
"del": "8.0.1", "del": "8.0.1",
"eslint": "9.39.1", "eslint": "9.38.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.10", "eslint-import-resolver-webpack": "0.13.10",
@@ -201,7 +201,7 @@
"gulp-rename": "2.1.0", "gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0", "html-minifier-terser": "7.2.0",
"husky": "9.1.7", "husky": "9.1.7",
"jsdom": "27.1.0", "jsdom": "27.0.1",
"jszip": "3.10.1", "jszip": "3.10.1",
"lint-staged": "16.2.6", "lint-staged": "16.2.6",
"lit-analyzer": "2.0.3", "lit-analyzer": "2.0.3",
@@ -213,13 +213,13 @@
"rspack-manifest-plugin": "5.1.0", "rspack-manifest-plugin": "5.1.0",
"serve": "14.2.5", "serve": "14.2.5",
"sinon": "21.0.0", "sinon": "21.0.0",
"tar": "7.5.2", "tar": "7.5.1",
"terser-webpack-plugin": "5.3.14", "terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.46.3", "typescript-eslint": "8.46.2",
"vite-tsconfig-paths": "5.1.4", "vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.7", "vitest": "4.0.1",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0", "webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -231,12 +231,9 @@
"clean-css": "5.3.3", "clean-css": "5.3.3",
"@lit/reactive-element": "2.1.1", "@lit/reactive-element": "2.1.1",
"@fullcalendar/daygrid": "6.1.19", "@fullcalendar/daygrid": "6.1.19",
"globals": "16.5.0", "globals": "16.4.0",
"tslib": "2.8.1", "tslib": "2.8.1",
"@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" "@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"
}, },
"packageManager": "yarn@4.10.3", "packageManager": "yarn@4.10.3"
"volta": {
"node": "22.21.1"
}
} }

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20251029.0" version = "20250924.0"
license = "Apache-2.0" license = "Apache-2.0"
license-files = ["LICENSE*"] license-files = ["LICENSE*"]
description = "The Home Assistant frontend" description = "The Home Assistant frontend"

View File

@@ -1,5 +1,4 @@
#!/bin/sh #!/bin/sh
# Pushes a new version to PyPi.
# Stop on errors # Stop on errors
set -e set -e
@@ -12,5 +11,4 @@ yarn install
script/build_frontend script/build_frontend
rm -rf dist home_assistant_frontend.egg-info rm -rf dist home_assistant_frontend.egg-info
python3 -m build python3 -m build -q
python3 -m twine upload dist/*.whl --skip-existing

View File

@@ -1,5 +1,4 @@
/* eslint-disable lit/prefer-static-styles */ /* eslint-disable lit/prefer-static-styles */
import { mdiOpenInNew } from "@mdi/js";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@@ -7,8 +6,6 @@ import punycode from "punycode";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { extractSearchParamsObject } from "../common/url/search-params"; import { extractSearchParamsObject } from "../common/url/search-params";
import "../components/ha-alert"; import "../components/ha-alert";
import "../components/ha-button";
import "../components/ha-svg-icon";
import type { AuthProvider, AuthUrlSearchParams } from "../data/auth"; import type { AuthProvider, AuthUrlSearchParams } from "../data/auth";
import { fetchAuthProviders } from "../data/auth"; import { fetchAuthProviders } from "../data/auth";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
@@ -136,8 +133,25 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.footer ha-svg-icon { ha-language-picker {
--mdc-icon-size: var(--ha-space-5); width: 200px;
border-radius: var(--ha-border-radius-sm);
overflow: hidden;
--ha-select-height: 40px;
--mdc-select-fill-color: none;
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
--mdc-select-ink-color: var(--primary-text-color, #212121);
--mdc-select-idle-line-color: transparent;
--mdc-select-hover-line-color: transparent;
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
--mdc-shape-small: 0;
}
.footer a {
text-decoration: none;
color: var(--primary-text-color);
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
} }
h1 { h1 {
font-size: var(--ha-font-size-3xl); font-size: var(--ha-font-size-3xl);
@@ -191,21 +205,16 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
<ha-language-picker <ha-language-picker
.value=${this.language} .value=${this.language}
.label=${""} .label=${""}
button-style
native-name native-name
@value-changed=${this._languageChanged} @value-changed=${this._languageChanged}
inline-arrow inline-arrow
></ha-language-picker> ></ha-language-picker>
<ha-button <a
appearance="plain"
variant="neutral"
href="https://www.home-assistant.io/docs/authentication/" href="https://www.home-assistant.io/docs/authentication/"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
>${this.localize("ui.panel.page-authorize.help")}</a
> >
${this.localize("ui.panel.page-authorize.help")}
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-button>
</div> </div>
`; `;
} }

View File

@@ -9,9 +9,9 @@ type EntityCategory = "none" | "config" | "diagnostic";
export interface EntityFilter { export interface EntityFilter {
domain?: string | string[]; domain?: string | string[];
device_class?: string | string[]; device_class?: string | string[];
device?: string | null | (string | null)[]; device?: string | string[];
area?: string | null | (string | null)[]; area?: string | string[];
floor?: string | null | (string | null)[]; floor?: string | string[];
label?: string | string[]; label?: string | string[];
entity_category?: EntityCategory | EntityCategory[]; entity_category?: EntityCategory | EntityCategory[];
hidden_platform?: string | string[]; hidden_platform?: string | string[];
@@ -19,18 +19,6 @@ export interface EntityFilter {
export type EntityFilterFunc = (entityId: string) => boolean; export type EntityFilterFunc = (entityId: string) => boolean;
const normalizeFilterArray = <T>(
value: T | null | T[] | (T | null)[] | undefined
): Set<T | null> | undefined => {
if (value === undefined) {
return undefined;
}
if (value === null) {
return new Set([null]);
}
return new Set(ensureArray(value));
};
export const generateEntityFilter = ( export const generateEntityFilter = (
hass: HomeAssistant, hass: HomeAssistant,
filter: EntityFilter filter: EntityFilter
@@ -41,9 +29,11 @@ export const generateEntityFilter = (
const deviceClasses = filter.device_class const deviceClasses = filter.device_class
? new Set(ensureArray(filter.device_class)) ? new Set(ensureArray(filter.device_class))
: undefined; : undefined;
const floors = normalizeFilterArray(filter.floor); const floors = filter.floor ? new Set(ensureArray(filter.floor)) : undefined;
const areas = normalizeFilterArray(filter.area); const areas = filter.area ? new Set(ensureArray(filter.area)) : undefined;
const devices = normalizeFilterArray(filter.device); const devices = filter.device
? new Set(ensureArray(filter.device))
: undefined;
const entityCategories = filter.entity_category const entityCategories = filter.entity_category
? new Set(ensureArray(filter.entity_category)) ? new Set(ensureArray(filter.entity_category))
: undefined; : undefined;
@@ -83,20 +73,23 @@ export const generateEntityFilter = (
} }
if (floors) { if (floors) {
const floorId = floor?.floor_id ?? null; if (!floor || !floors.has(floor.floor_id)) {
if (!floors.has(floorId)) {
return false; return false;
} }
} }
if (areas) { if (areas) {
const areaId = area?.area_id ?? null; if (!area) {
if (!areas.has(areaId)) { return false;
}
if (!areas.has(area.area_id)) {
return false; return false;
} }
} }
if (devices) { if (devices) {
const deviceId = device?.id ?? null; if (!device) {
if (!devices.has(deviceId)) { return false;
}
if (!devices.has(device.id)) {
return false; return false;
} }
} }

View File

@@ -214,7 +214,6 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
"pm1", "pm1",
"pm10", "pm10",
"pm25", "pm25",
"pm4",
"power_factor", "power_factor",
"power", "power",
"pressure", "pressure",

View File

@@ -1,116 +0,0 @@
export interface SwipeGestureResult {
velocity: number;
delta: number;
isSwipe: boolean;
isDownwardSwipe: boolean;
}
export interface SwipeGestureConfig {
velocitySwipeThreshold?: number;
movementTimeThreshold?: number;
}
const VELOCITY_SWIPE_THRESHOLD = 0.5; // px/ms
const MOVEMENT_TIME_THRESHOLD = 100; // ms
/**
* Recognizes swipe gestures and calculates velocity for touch interactions.
* Tracks touch movement and provides velocity-based and position-based gesture detection.
*/
export class SwipeGestureRecognizer {
private _startY = 0;
private _delta = 0;
private _startTime = 0;
private _lastY = 0;
private _lastTime = 0;
private _velocityThreshold: number;
private _movementTimeThreshold: number;
constructor(config: SwipeGestureConfig = {}) {
this._velocityThreshold =
config.velocitySwipeThreshold ?? VELOCITY_SWIPE_THRESHOLD; // px/ms
this._movementTimeThreshold =
config.movementTimeThreshold ?? MOVEMENT_TIME_THRESHOLD; // ms
}
/**
* Initialize gesture tracking with starting touch position
*/
public start(clientY: number): void {
const now = Date.now();
this._startY = clientY;
this._startTime = now;
this._lastY = clientY;
this._lastTime = now;
this._delta = 0;
}
/**
* Update gesture state during movement
* Returns the current delta (negative when dragging down)
*/
public move(clientY: number): number {
const now = Date.now();
this._delta = this._startY - clientY;
this._lastY = clientY;
this._lastTime = now;
return this._delta;
}
/**
* Calculate final gesture result when touch ends
*/
public end(): SwipeGestureResult {
const velocity = this.getVelocity();
const hasSignificantVelocity = Math.abs(velocity) > this._velocityThreshold;
return {
velocity,
delta: this._delta,
isSwipe: hasSignificantVelocity,
isDownwardSwipe: velocity > 0,
};
}
/**
* Get current drag delta (negative when dragging down)
*/
public getDelta(): number {
return this._delta;
}
/**
* Calculate velocity based on recent movement
* Returns 0 if no recent movement detected
* Positive velocity means downward swipe
*/
public getVelocity(): number {
const now = Date.now();
const timeSinceLastMove = now - this._lastTime;
// Only consider velocity if the last movement was recent
if (timeSinceLastMove >= this._movementTimeThreshold) {
return 0;
}
const timeDelta = this._lastTime - this._startTime;
return timeDelta > 0 ? (this._lastY - this._startY) / timeDelta : 0;
}
/**
* Reset all tracking state
*/
public reset(): void {
this._startY = 0;
this._delta = 0;
this._startTime = 0;
this._lastY = 0;
this._lastTime = 0;
}
}

View File

@@ -90,8 +90,6 @@ export class HaChartBase extends LitElement {
private _shouldResizeChart = false; private _shouldResizeChart = false;
private _resizeAnimationDuration?: number;
// @ts-ignore // @ts-ignore
private _resizeController = new ResizeController(this, { private _resizeController = new ResizeController(this, {
callback: () => { callback: () => {
@@ -207,16 +205,6 @@ export class HaChartBase extends LitElement {
} }
if (changedProps.has("options")) { if (changedProps.has("options")) {
chartOptions = { ...chartOptions, ...this._createOptions() }; chartOptions = { ...chartOptions, ...this._createOptions() };
if (
this._compareCustomLegendOptions(
changedProps.get("options"),
this.options
)
) {
// custom legend changes may require a resize to layout properly
this._shouldResizeChart = true;
this._resizeAnimationDuration = 250;
}
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) { } else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
chartOptions.dataZoom = this._getDataZoomConfig(); chartOptions.dataZoom = this._getDataZoomConfig();
} }
@@ -308,7 +296,7 @@ export class HaChartBase extends LitElement {
itemStyle = { itemStyle = {
color: dataset?.color as string, color: dataset?.color as string,
...(dataset?.itemStyle as { borderColor?: string }), ...(dataset?.itemStyle as { borderColor?: string }),
...itemStyle, itemStyle,
}; };
const color = itemStyle?.color as string; const color = itemStyle?.color as string;
const borderColor = itemStyle?.borderColor as string; const borderColor = itemStyle?.borderColor as string;
@@ -520,7 +508,6 @@ export class HaChartBase extends LitElement {
); );
} }
}); });
this.requestUpdate("_hiddenDatasets");
} }
private _getDataZoomConfig(): DataZoomComponentOption | undefined { private _getDataZoomConfig(): DataZoomComponentOption | undefined {
@@ -627,10 +614,6 @@ export class HaChartBase extends LitElement {
} }
private _createTheme(style: CSSStyleDeclaration) { private _createTheme(style: CSSStyleDeclaration) {
const textBorderColor =
style.getPropertyValue("--ha-card-background") ||
style.getPropertyValue("--card-background-color");
const textBorderWidth = 2;
return { return {
color: getAllGraphColors(style), color: getAllGraphColors(style),
backgroundColor: "transparent", backgroundColor: "transparent",
@@ -654,22 +637,15 @@ export class HaChartBase extends LitElement {
graph: { graph: {
label: { label: {
color: style.getPropertyValue("--primary-text-color"), color: style.getPropertyValue("--primary-text-color"),
textBorderColor, textBorderColor: style.getPropertyValue("--primary-background-color"),
textBorderWidth, textBorderWidth: 2,
},
},
pie: {
label: {
color: style.getPropertyValue("--primary-text-color"),
textBorderColor,
textBorderWidth,
}, },
}, },
sankey: { sankey: {
label: { label: {
color: style.getPropertyValue("--primary-text-color"), color: style.getPropertyValue("--primary-text-color"),
textBorderColor, textBorderColor: style.getPropertyValue("--primary-background-color"),
textBorderWidth, textBorderWidth: 2,
}, },
}, },
categoryAxis: { categoryAxis: {
@@ -982,34 +958,11 @@ export class HaChartBase extends LitElement {
private _handleChartRenderFinished = () => { private _handleChartRenderFinished = () => {
if (this._shouldResizeChart) { if (this._shouldResizeChart) {
this.chart?.resize({ this.chart?.resize();
animation:
this._reducedMotion ||
typeof this._resizeAnimationDuration !== "number"
? undefined
: { duration: this._resizeAnimationDuration },
});
this._shouldResizeChart = false; this._shouldResizeChart = false;
this._resizeAnimationDuration = undefined;
} }
}; };
private _compareCustomLegendOptions(
oldOptions: ECOption | undefined,
newOptions: ECOption | undefined
): boolean {
const oldLegends = ensureArray(
oldOptions?.legend || []
) as LegendComponentOption[];
const newLegends = ensureArray(
newOptions?.legend || []
) as LegendComponentOption[];
return (
oldLegends.some((l) => l.show && l.type === "custom") !==
newLegends.some((l) => l.show && l.type === "custom")
);
}
static styles = css` static styles = css`
:host { :host {
display: block; display: block;

View File

@@ -2,10 +2,7 @@ import type { EChartsType } from "echarts/core";
import type { GraphSeriesOption } from "echarts/charts"; import type { GraphSeriesOption } from "echarts/charts";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import type { import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
CallbackDataParams,
TopLevelFormatterParams,
} from "echarts/types/dist/shared";
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js"; import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { listenMediaQuery } from "../../common/dom/media_query"; import { listenMediaQuery } from "../../common/dom/media_query";
@@ -19,7 +16,6 @@ import { deepEqual } from "../../common/util/deep-equal";
export interface NetworkNode { export interface NetworkNode {
id: string; id: string;
name?: string; name?: string;
context?: string;
category?: number; category?: number;
value?: number; value?: number;
symbolSize?: number; symbolSize?: number;
@@ -192,25 +188,6 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
label: { label: {
show: showLabels, show: showLabels,
position: "right", position: "right",
formatter: (params: CallbackDataParams) => {
const node = params.data as NetworkNode;
if (node.context) {
return `{primary|${node.name ?? ""}}\n{secondary|${node.context}}`;
}
return node.name ?? "";
},
rich: {
primary: {
fontSize: 12,
},
secondary: {
fontSize: 12,
color: getComputedStyle(document.body).getPropertyValue(
"--secondary-text-color"
),
lineHeight: 16,
},
},
}, },
emphasis: { emphasis: {
focus: isMobile ? "none" : "adjacency", focus: isMobile ? "none" : "adjacency",
@@ -248,7 +225,6 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
({ ({
id: node.id, id: node.id,
name: node.name, name: node.name,
context: node.context,
category: node.category, category: node.category,
value: node.value, value: node.value,
symbolSize: node.symbolSize || 30, symbolSize: node.symbolSize || 30,

View File

@@ -87,8 +87,6 @@ export class StateHistoryChartLine extends LitElement {
private _previousYAxisLabelValue = 0; private _previousYAxisLabelValue = 0;
private _yAxisMaximumFractionDigits = 0;
protected render() { protected render() {
return html` return html`
<ha-chart-base <ha-chart-base
@@ -759,12 +757,8 @@ export class StateHistoryChartLine extends LitElement {
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1)) Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
) )
); );
this._yAxisMaximumFractionDigits = Math.max(
this._yAxisMaximumFractionDigits,
maximumFractionDigits
);
const label = formatNumber(value, this.hass.locale, { const label = formatNumber(value, this.hass.locale, {
maximumFractionDigits: this._yAxisMaximumFractionDigits, maximumFractionDigits,
}); });
const width = measureTextWidth(label, 12) + 5; const width = measureTextWidth(label, 12) + 5;
if (width > this._yWidth) { if (width > this._yWidth) {

View File

@@ -1 +0,0 @@
export const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";

View File

@@ -147,7 +147,7 @@ class HaEntitiesPicker extends LitElement {
.createDomains=${this.createDomains} .createDomains=${this.createDomains}
.required=${this.required && !currentEntities.length} .required=${this.required && !currentEntities.length}
@value-changed=${this._addEntity} @value-changed=${this._addEntity}
.addButton=${currentEntities.length > 0} add-button
></ha-entity-picker> ></ha-entity-picker>
</div> </div>
`; `;

View File

@@ -312,7 +312,7 @@ export class HaEntityNamePicker extends LitElement {
private _toValue = memoizeOne( private _toValue = memoizeOne(
(items: EntityNameItem[]): typeof this.value => { (items: EntityNameItem[]): typeof this.value => {
if (items.length === 0) { if (items.length === 0) {
return undefined; return "";
} }
if (items.length === 1) { if (items.length === 1) {
const item = items[0]; const item = items[0];

View File

@@ -4,7 +4,6 @@ import { customElement, property } from "lit/decorators";
import { keyed } from "lit/directives/keyed"; import { keyed } from "lit/directives/keyed";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { ANY_STATE_VALUE } from "./const";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "./ha-entity-state-picker"; import "./ha-entity-state-picker";
@@ -58,7 +57,6 @@ export class HaEntityStatesPicker extends LitElement {
const value = this.value || []; const value = this.value || [];
const hide = [...(this.hideStates || []), ...value]; const hide = [...(this.hideStates || []), ...value];
const hideValue = value.includes(ANY_STATE_VALUE);
return html` return html`
${repeat( ${repeat(
@@ -86,7 +84,7 @@ export class HaEntityStatesPicker extends LitElement {
` `
)} )}
<div> <div>
${(this.disabled && value.length) || hideValue ${this.disabled && value.length
? nothing ? nothing
: keyed( : keyed(
value.length, value.length,

View File

@@ -87,8 +87,6 @@ export class HaAreaPicker extends LitElement {
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
@query("ha-generic-picker") private _picker?: HaGenericPicker; @query("ha-generic-picker") private _picker?: HaGenericPicker;
public async open() { public async open() {
@@ -377,7 +375,6 @@ export class HaAreaPicker extends LitElement {
.getItems=${this._getItems} .getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems} .getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer} .valueRenderer=${valueRenderer}
.addButtonLabel=${this.addButtonLabel}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
</ha-generic-picker> </ha-generic-picker>

View File

@@ -118,7 +118,7 @@ export class HaAutomationRow extends LitElement {
} }
.row { .row {
display: flex; display: flex;
padding: var(--ha-space-0) var(--ha-space-2); padding: 0 8px;
min-height: 48px; min-height: 48px;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
@@ -134,12 +134,12 @@ export class HaAutomationRow extends LitElement {
.expand-button { .expand-button {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--ha-color-on-neutral-quiet); color: var(--ha-color-on-neutral-quiet);
margin-left: calc(var(--ha-space-2) * -1); margin-left: -8px;
} }
:host([building-block]) .leading-icon-wrapper { :host([building-block]) .leading-icon-wrapper {
background-color: var(--ha-color-fill-neutral-loud-resting); background-color: var(--ha-color-fill-neutral-loud-resting);
border-radius: var(--ha-border-radius-md); border-radius: var(--ha-border-radius-md);
padding: var(--ha-space-1); padding: 4px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -149,7 +149,7 @@ export class HaAutomationRow extends LitElement {
color: var(--ha-color-on-neutral-quiet); color: var(--ha-color-on-neutral-quiet);
} }
:host([building-block]) ::slotted([slot="leading-icon"]) { :host([building-block]) ::slotted([slot="leading-icon"]) {
--mdc-icon-size: var(--ha-space-5); --mdc-icon-size: 20px;
color: var(--white-color); color: var(--white-color);
transform: rotate(-45deg); transform: rotate(-45deg);
} }
@@ -170,7 +170,7 @@ export class HaAutomationRow extends LitElement {
::slotted([slot="header"]) { ::slotted([slot="header"]) {
flex: 1; flex: 1;
overflow-wrap: anywhere; overflow-wrap: anywhere;
margin: var(--ha-space-0) var(--ha-space-3); margin: 0 12px;
} }
:host([sort-selected]) .row { :host([sort-selected]) .row {
outline: solid; outline: solid;

View File

@@ -1,7 +1,6 @@
import "@home-assistant/webawesome/dist/components/drawer/drawer"; import "@home-assistant/webawesome/dist/components/drawer/drawer";
import { css, html, LitElement, type PropertyValues } from "lit"; import { css, html, LitElement, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300; export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
@@ -15,12 +14,6 @@ export class HaBottomSheet extends LitElement {
@state() private _drawerOpen = false; @state() private _drawerOpen = false;
@query("#drawer") private _drawer!: HTMLElement;
private _gestureRecognizer = new SwipeGestureRecognizer();
private _isDragging = false;
private _handleAfterHide() { private _handleAfterHide() {
this.open = false; this.open = false;
const ev = new Event("closed", { const ev = new Event("closed", {
@@ -40,132 +33,19 @@ export class HaBottomSheet extends LitElement {
render() { render() {
return html` return html`
<wa-drawer <wa-drawer
id="drawer"
placement="bottom" placement="bottom"
.open=${this._drawerOpen} .open=${this._drawerOpen}
@wa-after-hide=${this._handleAfterHide} @wa-after-hide=${this._handleAfterHide}
without-header without-header
@touchstart=${this._handleTouchStart}
> >
<slot name="header"></slot> <slot name="header"></slot>
<div id="body" class="body ha-scrollbar"> <div class="body ha-scrollbar">
<slot></slot> <slot></slot>
</div> </div>
</wa-drawer> </wa-drawer>
`; `;
} }
private _handleTouchStart = (ev: TouchEvent) => {
// Check if any element inside drawer in the composed path has scrollTop > 0
for (const path of ev.composedPath()) {
const el = path as HTMLElement;
if (el === this._drawer) {
break;
}
if (el.scrollTop > 0) {
return;
}
}
this._startResizing(ev.touches[0].clientY);
};
private _startResizing(clientY: number) {
// register event listeners for drag handling
document.addEventListener("touchmove", this._handleTouchMove, {
passive: false,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
this._gestureRecognizer.start(clientY);
}
private _handleTouchMove = (ev: TouchEvent) => {
const currentY = ev.touches[0].clientY;
const delta = this._gestureRecognizer.move(currentY);
if (delta < 0) {
ev.preventDefault();
this._isDragging = true;
requestAnimationFrame(() => {
if (this._isDragging) {
this.style.setProperty(
"--dialog-transform",
`translateY(${delta * -1}px)`
);
}
});
}
};
private _animateSnapBack() {
// Add transition for smooth animation
this.style.setProperty(
"--dialog-transition",
`transform ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms ease-out`
);
// Reset transform to snap back
this.style.removeProperty("--dialog-transform");
// Remove transition after animation completes
setTimeout(() => {
this.style.removeProperty("--dialog-transition");
}, BOTTOM_SHEET_ANIMATION_DURATION_MS);
}
private _handleTouchEnd = () => {
this._unregisterResizeHandlers();
this._isDragging = false;
const result = this._gestureRecognizer.end();
// If velocity exceeds threshold, use velocity direction to determine action
if (result.isSwipe) {
if (result.isDownwardSwipe) {
// Downward swipe - close the bottom sheet
this._drawerOpen = false;
} else {
// Upward swipe - keep open and animate back
this._animateSnapBack();
}
return;
}
// If velocity is below threshold, use position-based logic
// Get the drawer height to calculate 50% threshold
const drawerBody = this._drawer.shadowRoot?.querySelector(
'[part="body"]'
) as HTMLElement;
const drawerHeight = drawerBody?.offsetHeight || 0;
// delta is negative when dragging down
// Close if dragged down past 50% of the drawer height
if (
drawerHeight > 0 &&
result.delta < 0 &&
Math.abs(result.delta) > drawerHeight * 0.5
) {
this._drawerOpen = false;
} else {
this._animateSnapBack();
}
};
private _unregisterResizeHandlers = () => {
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
};
disconnectedCallback() {
super.disconnectedCallback();
this._unregisterResizeHandlers();
this._isDragging = false;
}
static styles = [ static styles = [
haStyleScrollbar, haStyleScrollbar,
css` css`
@@ -179,8 +59,6 @@ export class HaBottomSheet extends LitElement {
wa-drawer::part(dialog) { wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh); max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center; align-items: center;
transform: var(--dialog-transform);
transition: var(--dialog-transition);
} }
wa-drawer::part(body) { wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width); max-width: var(--ha-bottom-sheet-max-width);
@@ -212,11 +90,6 @@ export class HaBottomSheet extends LitElement {
max-width: 100%; max-width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
} }
`, `,
]; ];

View File

@@ -59,7 +59,6 @@ export class HaButton extends Button {
line-height: 1; line-height: 1;
transition: background-color 0.15s ease-in-out; transition: background-color 0.15s ease-in-out;
text-wrap: wrap;
} }
:host([size="small"]) .button { :host([size="small"]) .button {

View File

@@ -44,26 +44,26 @@ export class HaCard extends LitElement {
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl)); font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em; letter-spacing: -0.012em;
line-height: var(--ha-line-height-expanded); line-height: var(--ha-line-height-expanded);
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4); padding: 12px 16px 16px;
display: block; display: block;
margin-block-start: var(--ha-space-0); margin-block-start: 0px;
margin-block-end: var(--ha-space-0); margin-block-end: 0px;
font-weight: var(--ha-font-weight-normal); font-weight: var(--ha-font-weight-normal);
} }
:host ::slotted(.card-content:not(:first-child)), :host ::slotted(.card-content:not(:first-child)),
slot:not(:first-child)::slotted(.card-content) { slot:not(:first-child)::slotted(.card-content) {
padding-top: var(--ha-space-0); padding-top: 0px;
margin-top: calc(var(--ha-space-2) * -1); margin-top: -8px;
} }
:host ::slotted(.card-content) { :host ::slotted(.card-content) {
padding: var(--ha-space-4); padding: 16px;
} }
:host ::slotted(.card-actions) { :host ::slotted(.card-actions) {
border-top: 1px solid var(--divider-color, #e8e8e8); border-top: 1px solid var(--divider-color, #e8e8e8);
padding: var(--ha-space-2); padding: 8px;
} }
`; `;

View File

@@ -148,7 +148,7 @@ export class HaForm extends LitElement implements HaFormElement {
.value=${getValue(this.data, item)} .value=${getValue(this.data, item)}
.label=${this._computeLabel(item, this.data)} .label=${this._computeLabel(item, this.data)}
.disabled=${item.disabled || this.disabled || false} .disabled=${item.disabled || this.disabled || false}
.placeholder=${item.required ? undefined : item.default} .placeholder=${item.required ? "" : item.default}
.helper=${this._computeHelper(item)} .helper=${this._computeHelper(item)}
.localizeValue=${this.localizeValue} .localizeValue=${this.localizeValue}
.required=${item.required || false} .required=${item.required || false}

View File

@@ -10,6 +10,7 @@ import type { HomeAssistant } from "../types";
import "./ha-bottom-sheet"; import "./ha-bottom-sheet";
import "./ha-button"; import "./ha-button";
import "./ha-combo-box-item"; import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-input-helper-text"; import "./ha-input-helper-text";
import "./ha-picker-combo-box"; import "./ha-picker-combo-box";
import type { import type {
@@ -23,7 +24,7 @@ import "./ha-svg-icon";
@customElement("ha-generic-picker") @customElement("ha-generic-picker")
export class HaGenericPicker extends LitElement { export class HaGenericPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes // eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false; @property({ type: Boolean }) public autofocus = false;
@@ -67,21 +68,6 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "not-found-label", type: String }) @property({ attribute: "not-found-label", type: String })
public notFoundLabel?: string; public notFoundLabel?: string;
@property({ attribute: "popover-placement" })
public popoverPlacement:
| "bottom"
| "top"
| "left"
| "right"
| "top-start"
| "top-end"
| "right-start"
| "right-end"
| "bottom-start"
| "bottom-end"
| "left-start"
| "left-end" = "bottom-start";
/** If set picker shows an add button instead of textbox when value isn't set */ /** If set picker shows an add button instead of textbox when value isn't set */
@property({ attribute: "add-button-label" }) public addButtonLabel?: string; @property({ attribute: "add-button-label" }) public addButtonLabel?: string;
@@ -149,7 +135,7 @@ export class HaGenericPicker extends LitElement {
style="--body-width: ${this._popoverWidth}px;" style="--body-width: ${this._popoverWidth}px;"
without-arrow without-arrow
distance="-4" distance="-4"
.placement=${this.popoverPlacement} placement="bottom-start"
for="picker" for="picker"
auto-size="vertical" auto-size="vertical"
auto-size-padding="16" auto-size-padding="16"
@@ -158,7 +144,9 @@ export class HaGenericPicker extends LitElement {
trap-focus trap-focus
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label=${this.label || "Select option"} aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
> >
${this._renderComboBox()} ${this._renderComboBox()}
</wa-popover> </wa-popover>
@@ -171,7 +159,9 @@ export class HaGenericPicker extends LitElement {
@closed=${this._hidePicker} @closed=${this._hidePicker}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label=${this.label || "Select option"} aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
> >
${this._renderComboBox(true)} ${this._renderComboBox(true)}
</ha-bottom-sheet>` </ha-bottom-sheet>`
@@ -189,8 +179,7 @@ export class HaGenericPicker extends LitElement {
<ha-picker-combo-box <ha-picker-combo-box
.hass=${this.hass} .hass=${this.hass}
.allowCustomValue=${this.allowCustomValue} .allowCustomValue=${this.allowCustomValue}
.label=${this.searchLabel ?? .label=${this.searchLabel ?? this.hass.localize("ui.common.search")}
(this.hass?.localize("ui.common.search") || "Search")}
.value=${this.value} .value=${this.value}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.rowRenderer=${this.rowRenderer} .rowRenderer=${this.rowRenderer}

View File

@@ -1,59 +1,56 @@
import { mdiMenuDown } from "@mdi/js";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { formatLanguageCode } from "../common/language/format_language"; import { formatLanguageCode } from "../common/language/format_language";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { FrontendLocaleData } from "../data/translation"; import type { FrontendLocaleData } from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata"; import { translationMetadata } from "../resources/translations-metadata";
import type { HomeAssistant, ValueChangedEvent } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-button"; import "./ha-list-item";
import "./ha-generic-picker"; import "./ha-select";
import type { HaGenericPicker } from "./ha-generic-picker"; import type { HaSelect } from "./ha-select";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
export const getLanguageOptions = ( export const getLanguageOptions = (
languages: string[], languages: string[],
nativeName: boolean, nativeName: boolean,
noSort: boolean, noSort: boolean,
locale?: FrontendLocaleData locale?: FrontendLocaleData
): PickerComboBoxItem[] => { ) => {
let options: PickerComboBoxItem[] = []; let options: { label: string; value: string }[] = [];
if (nativeName) { if (nativeName) {
const translations = translationMetadata.translations; const translations = translationMetadata.translations;
options = languages.map((lang) => { options = languages.map((lang) => {
let primary = translations[lang]?.nativeName; let label = translations[lang]?.nativeName;
if (!primary) { if (!label) {
try { try {
// this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user // this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
primary = new Intl.DisplayNames(lang, { label = new Intl.DisplayNames(lang, {
type: "language", type: "language",
fallback: "code", fallback: "code",
}).of(lang)!; }).of(lang)!;
} catch (_err) { } catch (_err) {
primary = lang; label = lang;
} }
} }
return { return {
id: lang, value: lang,
primary, label,
search_labels: [primary],
}; };
}); });
} else if (locale) { } else if (locale) {
options = languages.map((lang) => ({ options = languages.map((lang) => ({
id: lang, value: lang,
primary: formatLanguageCode(lang, locale), label: formatLanguageCode(lang, locale),
search_labels: [formatLanguageCode(lang, locale)],
})); }));
} }
if (!noSort && locale) { if (!noSort && locale) {
options.sort((a, b) => options.sort((a, b) =>
caseInsensitiveStringCompare(a.primary, b.primary, locale.language) caseInsensitiveStringCompare(a.label, b.label, locale.language)
); );
} }
return options; return options;
@@ -76,9 +73,6 @@ export class HaLanguagePicker extends LitElement {
@property({ attribute: "native-name", type: Boolean }) @property({ attribute: "native-name", type: Boolean })
public nativeName = false; public nativeName = false;
@property({ type: Boolean, attribute: "button-style" })
public buttonStyle = false;
@property({ attribute: "no-sort", type: Boolean }) public noSort = false; @property({ attribute: "no-sort", type: Boolean }) public noSort = false;
@property({ attribute: "inline-arrow", type: Boolean }) @property({ attribute: "inline-arrow", type: Boolean })
@@ -86,90 +80,115 @@ export class HaLanguagePicker extends LitElement {
@state() _defaultLanguages: string[] = []; @state() _defaultLanguages: string[] = [];
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker; @query("ha-select") private _select!: HaSelect;
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._computeDefaultLanguageOptions(); this._computeDefaultLanguageOptions();
} }
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
const localeChanged =
changedProperties.has("hass") &&
this.hass &&
changedProperties.get("hass") &&
changedProperties.get("hass").locale.language !==
this.hass.locale.language;
if (
changedProperties.has("languages") ||
changedProperties.has("value") ||
localeChanged
) {
this._select.layoutOptions();
if (!this.disabled && this._select.value !== this.value) {
fireEvent(this, "value-changed", { value: this._select.value });
}
if (!this.value) {
return;
}
const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.nativeName,
this.noSort,
this.hass?.locale
);
const selectedItemIndex = languageOptions.findIndex(
(option) => option.value === this.value
);
if (selectedItemIndex === -1) {
this.value = undefined;
}
if (localeChanged) {
this._select.select(selectedItemIndex);
}
}
}
private _getLanguagesOptions = memoizeOne(getLanguageOptions); private _getLanguagesOptions = memoizeOne(getLanguageOptions);
private _computeDefaultLanguageOptions() { private _computeDefaultLanguageOptions() {
this._defaultLanguages = Object.keys(translationMetadata.translations); this._defaultLanguages = Object.keys(translationMetadata.translations);
} }
private _getItems = () => protected render() {
this._getLanguagesOptions( const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages, this.languages ?? this._defaultLanguages,
this.nativeName, this.nativeName,
this.noSort, this.noSort,
this.hass?.locale this.hass?.locale
); );
private _getLanguageName = (lang?: string) =>
this._getItems().find((language) => language.id === lang)?.primary;
private _valueRenderer = (value) =>
html`<span slot="headline"
>${this._getLanguageName(value) ?? value}</span
> `;
protected render() {
const value = const value =
this.value ?? this.value ??
(this.required && !this.disabled ? this._getItems()[0].id : this.value); (this.required && !this.disabled
? languageOptions[0]?.value
: this.value);
return html` return html`
<ha-generic-picker <ha-select
.hass=${this.hass} .label=${this.label ??
.autofocus=${this.autofocus}
popover-placement="bottom-end"
.notFoundLabel=${this.hass?.localize(
"ui.components.language-picker.no_match"
)}
.placeholder=${this.label ??
(this.hass?.localize("ui.components.language-picker.language") || (this.hass?.localize("ui.components.language-picker.language") ||
"Language")} "Language")}
.value=${value} .value=${value || ""}
.valueRenderer=${this._valueRenderer} .required=${this.required}
.disabled=${this.disabled} .disabled=${this.disabled}
.getItems=${this._getItems} @selected=${this._changed}
@value-changed=${this._changed} @closed=${stopPropagation}
hide-clear-icon fixedMenuPosition
naturalMenuWidth
.inlineArrow=${this.inlineArrow}
> >
${this.buttonStyle ${languageOptions.length === 0
? html`<ha-button ? html`<ha-list-item value=""
slot="field" >${this.hass?.localize(
.disabled=${this.disabled} "ui.components.language-picker.no_languages"
@click=${this._openPicker} ) || "No languages"}</ha-list-item
appearance="plain" >`
variant="neutral" : languageOptions.map(
(option) => html`
<ha-list-item .value=${option.value}
>${option.label}</ha-list-item
> >
${this._getLanguageName(value)} `
<ha-svg-icon slot="end" .path=${mdiMenuDown}></ha-svg-icon> )}
</ha-button>` </ha-select>
: nothing}
</ha-generic-picker>
`; `;
} }
private _openPicker(ev: Event) {
ev.stopPropagation();
this.genericPicker.open();
}
static styles = css` static styles = css`
ha-generic-picker { ha-select {
width: 100%; width: 100%;
min-width: 200px;
display: block;
} }
`; `;
private _changed(ev: ValueChangedEvent<string>): void { private _changed(ev): void {
ev.stopPropagation(); const target = ev.target as HaSelect;
this.value = ev.detail.value; if (this.disabled || target.value === "" || target.value === this.value) {
return;
}
this.value = target.value;
fireEvent(this, "value-changed", { value: this.value }); fireEvent(this, "value-changed", { value: this.value });
} }
} }

View File

@@ -50,7 +50,7 @@ export class HaMarkdown extends LitElement {
} }
ha-alert { ha-alert {
display: block; display: block;
margin: var(--ha-space-1) 0; margin: 4px 0;
} }
a { a {
color: var(--primary-color); color: var(--primary-color);
@@ -75,7 +75,7 @@ export class HaMarkdown extends LitElement {
padding: 0; padding: 0;
} }
pre { pre {
padding: var(--ha-space-4); padding: 16px;
overflow: auto; overflow: auto;
line-height: var(--ha-line-height-condensed); line-height: var(--ha-line-height-condensed);
font-family: var(--ha-font-family-code); font-family: var(--ha-font-family-code);
@@ -95,7 +95,7 @@ export class HaMarkdown extends LitElement {
hr { hr {
border-color: var(--divider-color); border-color: var(--divider-color);
border-bottom: none; border-bottom: none;
margin: var(--ha-space-4) 0; margin: 16px 0;
} }
` as CSSResultGroup; ` as CSSResultGroup;
} }

View File

@@ -69,7 +69,7 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
@customElement("ha-picker-combo-box") @customElement("ha-picker-combo-box")
export class HaPickerComboBox extends LitElement { export class HaPickerComboBox extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes // eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false; @property({ type: Boolean }) public autofocus = false;
@@ -140,9 +140,7 @@ export class HaPickerComboBox extends LitElement {
protected render() { protected render() {
return html`<ha-textfield return html`<ha-textfield
.label=${this.label ?? .label=${this.label ?? this.hass.localize("ui.common.search")}
this.hass?.localize("ui.common.search") ??
"Search"}
@input=${this._filterChanged} @input=${this._filterChanged}
></ha-textfield> ></ha-textfield>
<lit-virtualizer <lit-virtualizer
@@ -161,18 +159,12 @@ export class HaPickerComboBox extends LitElement {
private _defaultNotFoundItem = memoizeOne( private _defaultNotFoundItem = memoizeOne(
( (
label: this["notFoundLabel"], label: this["notFoundLabel"],
localize?: LocalizeFunc localize: LocalizeFunc
): PickerComboBoxItemWithLabel => ({ ): PickerComboBoxItemWithLabel => ({
id: NO_MATCHING_ITEMS_FOUND_ID, id: NO_MATCHING_ITEMS_FOUND_ID,
primary: primary: label || localize("ui.components.combo-box.no_match"),
label ||
(localize && localize("ui.components.combo-box.no_match")) ||
"No matching items found",
icon_path: mdiMagnify, icon_path: mdiMagnify,
a11y_label: a11y_label: label || localize("ui.components.combo-box.no_match"),
label ||
(localize && localize("ui.components.combo-box.no_match")) ||
"No matching items found",
}) })
); );
@@ -197,13 +189,13 @@ export class HaPickerComboBox extends LitElement {
caseInsensitiveStringCompare( caseInsensitiveStringCompare(
entityA.sorting_label!, entityA.sorting_label!,
entityB.sorting_label!, entityB.sorting_label!,
this.hass?.locale.language ?? navigator.language this.hass.locale.language
) )
); );
if (!sortedItems.length) { if (!sortedItems.length) {
sortedItems.push( sortedItems.push(
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize) this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
); );
} }
@@ -257,20 +249,8 @@ export class HaPickerComboBox extends LitElement {
const textfield = ev.target as HaTextField; const textfield = ev.target as HaTextField;
const searchString = textfield.value.trim(); const searchString = textfield.value.trim();
if (!searchString) {
this._items = this._allItems;
return;
}
const index = this._fuseIndex(this._allItems); const index = this._fuseIndex(this._allItems);
const fuse = new HaFuse( const fuse = new HaFuse(this._allItems, { shouldSort: false }, index);
this._allItems,
{
shouldSort: false,
minMatchCharLength: Math.min(searchString.length, 2),
},
index
);
const results = fuse.multiTermsSearch(searchString); const results = fuse.multiTermsSearch(searchString);
let filteredItems = this._allItems as PickerComboBoxItem[]; let filteredItems = this._allItems as PickerComboBoxItem[];
@@ -278,7 +258,7 @@ export class HaPickerComboBox extends LitElement {
const items = results.map((result) => result.item); const items = results.map((result) => result.item);
if (items.length === 0) { if (items.length === 0) {
items.push( items.push(
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize) this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
); );
} }
const additionalItems = this._getAdditionalItems(searchString); const additionalItems = this._getAdditionalItems(searchString);
@@ -451,17 +431,6 @@ export class HaPickerComboBox extends LitElement {
private _pickSelectedItem = (ev: KeyboardEvent) => { private _pickSelectedItem = (ev: KeyboardEvent) => {
ev.stopPropagation(); ev.stopPropagation();
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
if (
this._virtualizerElement?.items.length === 1 &&
firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID
) {
fireEvent(this, "value-changed", {
value: firstItem.id,
});
}
if (this._selectedItemIndex === -1) { if (this._selectedItemIndex === -1) {
return; return;
} }
@@ -469,9 +438,7 @@ export class HaPickerComboBox extends LitElement {
// if filter button is focused // if filter button is focused
ev.preventDefault(); ev.preventDefault();
const item = this._virtualizerElement?.items[ const item: any = this._virtualizerElement?.items[this._selectedItemIndex];
this._selectedItemIndex
] as PickerComboBoxItem;
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) { if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
fireEvent(this, "value-changed", { value: item.id }); fireEvent(this, "value-changed", { value: item.id });
} }

View File

@@ -0,0 +1,122 @@
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { BackgroundSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-picture-upload";
import "../ha-alert";
import type { HaPictureUpload } from "../ha-picture-upload";
import { URL_PREFIX } from "../../data/image_upload";
@customElement("ha-selector-background")
export class HaBackgroundSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: any;
@property({ attribute: false }) public selector!: BackgroundSelector;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@state() private yamlBackground = false;
protected updated(changedProps) {
super.updated(changedProps);
if (changedProps.has("value")) {
this.yamlBackground = !!this.value && !this.value.startsWith(URL_PREFIX);
}
}
protected render() {
return html`
<div>
${this.yamlBackground
? html`
<div class="value">
<img
src=${this.value}
alt=${this.hass.localize(
"ui.components.picture-upload.current_image_alt"
)}
/>
</div>
<ha-alert alert-type="info">
${this.hass.localize(
`ui.components.selectors.background.yaml_info`
)}
<ha-button slot="action" @click=${this._clearValue}>
${this.hass.localize(
`ui.components.picture-upload.clear_picture`
)}
</ha-button>
</ha-alert>
`
: html`
<ha-picture-upload
.hass=${this.hass}
.value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
.original=${!!this.selector.background?.original}
.cropOptions=${this.selector.background?.crop}
select-media
@change=${this._pictureChanged}
></ha-picture-upload>
`}
</div>
`;
}
private _pictureChanged(ev) {
const value = (ev.target as HaPictureUpload).value;
fireEvent(this, "value-changed", { value: value ?? undefined });
}
private _clearValue() {
fireEvent(this, "value-changed", { value: undefined });
}
static styles = css`
:host {
display: block;
position: relative;
}
ha-picture-upload {
background-color: var(--primary-background-color);
border-radius: var(--file-upload-image-border-radius);
}
div {
display: flex;
flex-direction: column;
}
ha-button {
white-space: nowrap;
--mdc-theme-primary: var(--primary-color);
}
.value {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
img {
max-width: 100%;
max-height: 200px;
margin-bottom: 4px;
border-radius: var(--file-upload-image-border-radius);
transition: opacity 0.3s;
opacity: var(--picture-opacity, 1);
}
img:hover {
opacity: 1;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-background": HaBackgroundSelector;
}
}

View File

@@ -34,6 +34,7 @@ const LOAD_ELEMENTS = {
file: () => import("./ha-selector-file"), file: () => import("./ha-selector-file"),
floor: () => import("./ha-selector-floor"), floor: () => import("./ha-selector-floor"),
label: () => import("./ha-selector-label"), label: () => import("./ha-selector-label"),
background: () => import("./ha-selector-background"),
language: () => import("./ha-selector-language"), language: () => import("./ha-selector-language"),
navigation: () => import("./ha-selector-navigation"), navigation: () => import("./ha-selector-navigation"),
number: () => import("./ha-selector-number"), number: () => import("./ha-selector-number"),

View File

@@ -76,16 +76,11 @@ class HaServicePicker extends LitElement {
</ha-combo-box-item> </ha-combo-box-item>
`; `;
private _valueRenderer = memoizeOne( private _valueRenderer: PickerValueRenderer = (value) => {
(
localize: LocalizeFunc,
services: HomeAssistant["services"]
): PickerValueRenderer =>
(value) => {
const serviceId = value; const serviceId = value;
const [domain, service] = serviceId.split("."); const [domain, service] = serviceId.split(".");
if (!services[domain]?.[service]) { if (!this.hass.services[domain]?.[service]) {
return html` return html`
<ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon>
<span slot="headline">${value}</span> <span slot="headline">${value}</span>
@@ -93,8 +88,8 @@ class HaServicePicker extends LitElement {
} }
const serviceName = const serviceName =
localize(`component.${domain}.services.${service}.name`) || this.hass.localize(`component.${domain}.services.${service}.name`) ||
services[domain][service].name || this.hass.services[domain][service].name ||
service; service;
return html` return html`
@@ -105,13 +100,10 @@ class HaServicePicker extends LitElement {
></ha-service-icon> ></ha-service-icon>
<span slot="headline">${serviceName}</span> <span slot="headline">${serviceName}</span>
${this.showServiceId ${this.showServiceId
? html`<span slot="supporting-text" class="code" ? html`<span slot="supporting-text" class="code">${serviceId}</span>`
>${serviceId}</span
>`
: nothing} : nothing}
`; `;
} };
);
protected render(): TemplateResult { protected render(): TemplateResult {
const placeholder = const placeholder =
@@ -131,10 +123,7 @@ class HaServicePicker extends LitElement {
.value=${this.value} .value=${this.value}
.getItems=${this._getItems} .getItems=${this._getItems}
.rowRenderer=${this._rowRenderer} .rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer( .valueRenderer=${this._valueRenderer}
this.hass.localize,
this.hass.services
)}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
</ha-generic-picker> </ha-generic-picker>

View File

@@ -157,8 +157,7 @@ export const computePanels = memoizeOne(
Object.values(panels).forEach((panel) => { Object.values(panels).forEach((panel) => {
if ( if (
hiddenPanels.includes(panel.url_path) || hiddenPanels.includes(panel.url_path) ||
(!panel.title && panel.url_path !== defaultPanel) || (!panel.title && panel.url_path !== defaultPanel)
(!panel.default_visible && !panelsOrder.includes(panel.url_path))
) { ) {
return; return;
} }

View File

@@ -87,40 +87,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
protected render() { protected render() {
if (this.addOnTop) { if (this.addOnTop) {
return html` ${this._renderPicker()} ${this._renderItems()} `; return html` ${this._renderChips()} ${this._renderItems()} `;
} }
return html` ${this._renderItems()} ${this._renderPicker()} `; return html` ${this._renderItems()} ${this._renderChips()} `;
} }
private _renderValueChips() { private _renderValueChips() {
const entityIds = this.value?.entity_id return html`<div class="mdc-chip-set items">
? ensureArray(this.value.entity_id) ${this.value?.floor_id
: []; ? ensureArray(this.value.floor_id).map(
const deviceIds = this.value?.device_id
? ensureArray(this.value.device_id)
: [];
const areaIds = this.value?.area_id ? ensureArray(this.value.area_id) : [];
const floorIds = this.value?.floor_id
? ensureArray(this.value.floor_id)
: [];
const labelIds = this.value?.label_id
? ensureArray(this.value.label_id)
: [];
if (
!entityIds.length &&
!deviceIds.length &&
!areaIds.length &&
!floorIds.length &&
!labelIds.length
) {
return nothing;
}
return html`
<div class="mdc-chip-set items">
${floorIds.length
? floorIds.map(
(floor_id) => html` (floor_id) => html`
<ha-target-picker-value-chip <ha-target-picker-value-chip
.hass=${this.hass} .hass=${this.hass}
@@ -132,8 +107,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
` `
) )
: nothing} : nothing}
${areaIds.length ${this.value?.area_id
? areaIds.map( ? ensureArray(this.value.area_id).map(
(area_id) => html` (area_id) => html`
<ha-target-picker-value-chip <ha-target-picker-value-chip
.hass=${this.hass} .hass=${this.hass}
@@ -145,8 +120,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
` `
) )
: nothing} : nothing}
${deviceIds.length ${this.value?.device_id
? deviceIds.map( ? ensureArray(this.value.device_id).map(
(device_id) => html` (device_id) => html`
<ha-target-picker-value-chip <ha-target-picker-value-chip
.hass=${this.hass} .hass=${this.hass}
@@ -158,8 +133,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
` `
) )
: nothing} : nothing}
${entityIds.length ${this.value?.entity_id
? entityIds.map( ? ensureArray(this.value.entity_id).map(
(entity_id) => html` (entity_id) => html`
<ha-target-picker-value-chip <ha-target-picker-value-chip
.hass=${this.hass} .hass=${this.hass}
@@ -171,8 +146,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
` `
) )
: nothing} : nothing}
${labelIds.length ${this.value?.label_id
? labelIds.map( ? ensureArray(this.value.label_id).map(
(label_id) => html` (label_id) => html`
<ha-target-picker-value-chip <ha-target-picker-value-chip
.hass=${this.hass} .hass=${this.hass}
@@ -184,44 +159,18 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
` `
) )
: nothing} : nothing}
</div> </div>`;
`;
} }
private _renderValueGroups() { private _renderValueGroups() {
const entityIds = this.value?.entity_id return html`<div class="item-groups">
? ensureArray(this.value.entity_id) ${this.value?.entity_id
: [];
const deviceIds = this.value?.device_id
? ensureArray(this.value.device_id)
: [];
const areaIds = this.value?.area_id ? ensureArray(this.value.area_id) : [];
const floorIds = this.value?.floor_id
? ensureArray(this.value.floor_id)
: [];
const labelIds = this.value?.label_id
? ensureArray(this.value?.label_id)
: [];
if (
!entityIds.length &&
!deviceIds.length &&
!areaIds.length &&
!floorIds.length &&
!labelIds.length
) {
return nothing;
}
return html`
<div class="item-groups">
${entityIds.length
? html` ? html`
<ha-target-picker-item-group <ha-target-picker-item-group
@remove-target-item=${this._handleRemove} @remove-target-item=${this._handleRemove}
type="entity" type="entity"
.hass=${this.hass} .hass=${this.hass}
.items=${{ entity: entityIds }} .items=${{ entity: ensureArray(this.value?.entity_id) }}
.deviceFilter=${this.deviceFilter} .deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter} .entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
@@ -230,13 +179,13 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
</ha-target-picker-item-group> </ha-target-picker-item-group>
` `
: nothing} : nothing}
${deviceIds.length ${this.value?.device_id
? html` ? html`
<ha-target-picker-item-group <ha-target-picker-item-group
@remove-target-item=${this._handleRemove} @remove-target-item=${this._handleRemove}
type="device" type="device"
.hass=${this.hass} .hass=${this.hass}
.items=${{ device: deviceIds }} .items=${{ device: ensureArray(this.value?.device_id) }}
.deviceFilter=${this.deviceFilter} .deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter} .entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
@@ -245,15 +194,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
</ha-target-picker-item-group> </ha-target-picker-item-group>
` `
: nothing} : nothing}
${floorIds.length || areaIds.length ${this.value?.floor_id || this.value?.area_id
? html` ? html`
<ha-target-picker-item-group <ha-target-picker-item-group
@remove-target-item=${this._handleRemove} @remove-target-item=${this._handleRemove}
type="area" type="area"
.hass=${this.hass} .hass=${this.hass}
.items=${{ .items=${{
floor: floorIds, floor: ensureArray(this.value?.floor_id),
area: areaIds, area: ensureArray(this.value?.area_id),
}} }}
.deviceFilter=${this.deviceFilter} .deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter} .entityFilter=${this.entityFilter}
@@ -263,13 +212,13 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
</ha-target-picker-item-group> </ha-target-picker-item-group>
` `
: nothing} : nothing}
${labelIds.length ${this.value?.label_id
? html` ? html`
<ha-target-picker-item-group <ha-target-picker-item-group
@remove-target-item=${this._handleRemove} @remove-target-item=${this._handleRemove}
type="label" type="label"
.hass=${this.hass} .hass=${this.hass}
.items=${{ label: labelIds }} .items=${{ label: ensureArray(this.value?.label_id) }}
.deviceFilter=${this.deviceFilter} .deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter} .entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
@@ -278,17 +227,26 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
</ha-target-picker-item-group> </ha-target-picker-item-group>
` `
: nothing} : nothing}
</div> </div>`;
`;
} }
private _renderItems() { private _renderItems() {
if (
!this.value?.floor_id &&
!this.value?.area_id &&
!this.value?.device_id &&
!this.value?.entity_id &&
!this.value?.label_id
) {
return nothing;
}
return html` return html`
${this.compact ? this._renderValueChips() : this._renderValueGroups()} ${this.compact ? this._renderValueChips() : this._renderValueGroups()}
`; `;
} }
private _renderPicker() { private _renderChips() {
return html` return html`
<div class="add-target-wrapper"> <div class="add-target-wrapper">
<ha-button <ha-button
@@ -389,8 +347,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
this._pickerFilter = filter; this._pickerFilter = filter;
}; };
private _hidePicker(ev) { private _hidePicker() {
ev.stopPropagation();
this._open = false; this._open = false;
this._pickerWrapperOpen = false; this._pickerWrapperOpen = false;

View File

@@ -9,7 +9,7 @@ export class HaTooltip extends Tooltip {
@property({ attribute: "show-delay", type: Number }) showDelay = 150; @property({ attribute: "show-delay", type: Number }) showDelay = 150;
/** The amount of time to wait before hiding the tooltip when the user mouses out.. */ /** The amount of time to wait before hiding the tooltip when the user mouses out.. */
@property({ attribute: "hide-delay", type: Number }) hideDelay = 150; @property({ attribute: "hide-delay", type: Number }) hideDelay = 400;
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [

View File

@@ -1,4 +1,6 @@
import { css, html, LitElement } from "lit"; import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { import {
customElement, customElement,
eventOptions, eventOptions,
@@ -6,9 +8,6 @@ import {
query, query,
state, state,
} from "lit/decorators"; } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { mdiClose } from "@mdi/js";
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@@ -32,8 +31,6 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
* *
* @slot header - Replace the entire header area. * @slot header - Replace the entire header area.
* @slot headerNavigationIcon - Leading header action (e.g. close/back button). * @slot headerNavigationIcon - Leading header action (e.g. close/back button).
* @slot headerTitle - Custom title content (used when header-title is not set).
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus). * @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
* @slot - Dialog content body. * @slot - Dialog content body.
* @slot footer - Dialog footer content. * @slot footer - Dialog footer content.
@@ -55,8 +52,8 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
* @attr {boolean} open - Controls the dialog open state. * @attr {boolean} open - Controls the dialog open state.
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium". * @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false. * @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used. * @attr {string} header-title - Header title text when no custom title slot is provided.
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used. * @attr {string} header-subtitle - Header subtitle text when no custom subtitle slot is provided.
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below". * @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
* @attr {boolean} flexcontent - Makes the dialog body a flex container for flexible layouts. * @attr {boolean} flexcontent - Makes the dialog body a flex container for flexible layouts.
* *
@@ -75,12 +72,6 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
export class HaWaDialog extends LitElement { export class HaWaDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "aria-labelledby" })
public ariaLabelledBy?: string;
@property({ attribute: "aria-describedby" })
public ariaDescribedBy?: string;
@property({ type: Boolean, reflect: true }) @property({ type: Boolean, reflect: true })
public open = false; public open = false;
@@ -90,11 +81,11 @@ export class HaWaDialog extends LitElement {
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" }) @property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
public preventScrimClose = false; public preventScrimClose = false;
@property({ attribute: "header-title" }) @property({ type: String, attribute: "header-title" })
public headerTitle?: string; public headerTitle = "";
@property({ attribute: "header-subtitle" }) @property({ type: String, attribute: "header-subtitle" })
public headerSubtitle?: string; public headerSubtitle = "";
@property({ type: String, attribute: "header-subtitle-position" }) @property({ type: String, attribute: "header-subtitle-position" })
public headerSubtitlePosition: "above" | "below" = "below"; public headerSubtitlePosition: "above" | "below" = "below";
@@ -126,11 +117,6 @@ export class HaWaDialog extends LitElement {
.open=${this._open} .open=${this._open}
.lightDismiss=${!this.preventScrimClose} .lightDismiss=${!this.preventScrimClose}
without-header without-header
aria-labelledby=${ifDefined(
this.ariaLabelledBy ||
(this.headerTitle !== undefined ? "ha-wa-dialog-title" : undefined)
)}
aria-describedby=${ifDefined(this.ariaDescribedBy)}
@wa-show=${this._handleShow} @wa-show=${this._handleShow}
@wa-after-show=${this._handleAfterShow} @wa-after-show=${this._handleAfterShow}
@wa-after-hide=${this._handleAfterHide} @wa-after-hide=${this._handleAfterHide}
@@ -147,14 +133,14 @@ export class HaWaDialog extends LitElement {
.path=${mdiClose} .path=${mdiClose}
></ha-icon-button> ></ha-icon-button>
</slot> </slot>
${this.headerTitle !== undefined ${this.headerTitle
? html`<span slot="title" class="title" id="ha-wa-dialog-title"> ? html`<span slot="title" class="title">
${this.headerTitle} ${this.headerTitle}
</span>` </span>`
: html`<slot name="headerTitle" slot="title"></slot>`} : nothing}
${this.headerSubtitle !== undefined ${this.headerSubtitle
? html`<span slot="subtitle">${this.headerSubtitle}</span>` ? html`<span slot="subtitle">${this.headerSubtitle}</span>`
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`} : nothing}
<slot name="headerActionItems" slot="actionItems"></slot> <slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header> </ha-dialog-header>
</slot> </slot>

View File

@@ -6,7 +6,6 @@ import {
mdiLabel, mdiLabel,
mdiTextureBox, mdiTextureBox,
} from "@mdi/js"; } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing, type PropertyValues } from "lit"; import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -20,12 +19,9 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name"; import { computeEntityName } from "../../common/entity/compute_entity_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context"; import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
import type { AreaRegistryEntry } from "../../data/area_registry";
import { getConfigEntry } from "../../data/config_entries"; import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context"; import { labelsContext } from "../../data/context";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity"; import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label_registry"; import type { LabelRegistryEntry } from "../../data/label_registry";
import { import {
@@ -115,10 +111,10 @@ export class HaTargetPickerItemRow extends LitElement {
} }
protected render() { protected render() {
const { name, context, iconPath, fallbackIconPath, stateObject, notFound } = const { name, context, iconPath, fallbackIconPath, stateObject } =
this._itemData(this.type, this.itemId); this._itemData(this.type, this.itemId);
const showEntities = this.type !== "entity" && !notFound; const showEntities = this.type !== "entity";
const entries = this.parentEntries || this._entries; const entries = this.parentEntries || this._entries;
@@ -132,7 +128,7 @@ export class HaTargetPickerItemRow extends LitElement {
} }
return html` return html`
<ha-md-list-item type="text" class=${notFound ? "error" : ""}> <ha-md-list-item type="text">
<div class="icon" slot="start"> <div class="icon" slot="start">
${this.subEntry ${this.subEntry
? html` ? html`
@@ -152,15 +148,11 @@ export class HaTargetPickerItemRow extends LitElement {
/>` />`
: fallbackIconPath : fallbackIconPath
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>` ? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
: this.type === "entity" : stateObject
? html` ? html`
<ha-state-icon <ha-state-icon
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObject || .stateObj=${stateObject}
({
entity_id: this.itemId,
attributes: {},
} as HassEntity)}
> >
</ha-state-icon> </ha-state-icon>
` `
@@ -168,14 +160,8 @@ export class HaTargetPickerItemRow extends LitElement {
</div> </div>
<div slot="headline">${name}</div> <div slot="headline">${name}</div>
${notFound || (context && !this.hideContext) ${context && !this.hideContext
? html`<span slot="supporting-text" ? html`<span slot="supporting-text">${context}</span>`
>${notFound
? this.hass.localize(
`ui.components.target-picker.${this.type}_not_found`
)
: context}</span
>`
: nothing} : nothing}
${this._domainName && this.subEntry ${this._domainName && this.subEntry
? html`<span slot="supporting-text" class="domain" ? html`<span slot="supporting-text" class="domain"
@@ -488,28 +474,26 @@ export class HaTargetPickerItemRow extends LitElement {
private _itemData = memoizeOne((type: TargetType, item: string) => { private _itemData = memoizeOne((type: TargetType, item: string) => {
if (type === "floor") { if (type === "floor") {
const floor: FloorRegistryEntry | undefined = this.hass.floors?.[item]; const floor = this.hass.floors?.[item];
return { return {
name: floor?.name || item, name: floor?.name || item,
iconPath: floor?.icon, iconPath: floor?.icon,
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome, fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
notFound: !floor,
}; };
} }
if (type === "area") { if (type === "area") {
const area: AreaRegistryEntry | undefined = this.hass.areas?.[item]; const area = this.hass.areas?.[item];
return { return {
name: area?.name || item, name: area?.name || item,
context: area?.floor_id && this.hass.floors?.[area.floor_id]?.name, context: area.floor_id && this.hass.floors?.[area.floor_id]?.name,
iconPath: area?.icon, iconPath: area?.icon,
fallbackIconPath: mdiTextureBox, fallbackIconPath: mdiTextureBox,
notFound: !area,
}; };
} }
if (type === "device") { if (type === "device") {
const device: DeviceRegistryEntry | undefined = this.hass.devices?.[item]; const device = this.hass.devices?.[item];
if (device?.primary_config_entry) { if (device.primary_config_entry) {
this._getDeviceDomain(device.primary_config_entry); this._getDeviceDomain(device.primary_config_entry);
} }
@@ -517,25 +501,24 @@ export class HaTargetPickerItemRow extends LitElement {
name: device ? computeDeviceNameDisplay(device, this.hass) : item, name: device ? computeDeviceNameDisplay(device, this.hass) : item,
context: device?.area_id && this.hass.areas?.[device.area_id]?.name, context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
fallbackIconPath: mdiDevices, fallbackIconPath: mdiDevices,
notFound: !device,
}; };
} }
if (type === "entity") { if (type === "entity") {
this._setDomainName(computeDomain(item)); this._setDomainName(computeDomain(item));
const stateObject: HassEntity | undefined = this.hass.states[item]; const stateObject = this.hass.states[item];
const entityName = stateObject const entityName = computeEntityName(
? computeEntityName(stateObject, this.hass.entities, this.hass.devices) stateObject,
: item; this.hass.entities,
const { area, device } = stateObject this.hass.devices
? getEntityContext( );
const { area, device } = getEntityContext(
stateObject, stateObject,
this.hass.entities, this.hass.entities,
this.hass.devices, this.hass.devices,
this.hass.areas, this.hass.areas,
this.hass.floors this.hass.floors
) );
: { area: undefined, device: undefined };
const deviceName = device ? computeDeviceName(device) : undefined; const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined; const areaName = area ? computeAreaName(area) : undefined;
const context = [areaName, entityName ? deviceName : undefined] const context = [areaName, entityName ? deviceName : undefined]
@@ -545,19 +528,15 @@ export class HaTargetPickerItemRow extends LitElement {
name: entityName || deviceName || item, name: entityName || deviceName || item,
context, context,
stateObject, stateObject,
notFound: !stateObject && item !== "all",
}; };
} }
// type label // type label
const label: LabelRegistryEntry | undefined = this._labelRegistry.find( const label = this._labelRegistry.find((lab) => lab.label_id === item);
(lab) => lab.label_id === item
);
return { return {
name: label?.name || item, name: label?.name || item,
iconPath: label?.icon, iconPath: label?.icon,
fallbackIconPath: mdiLabel, fallbackIconPath: mdiLabel,
notFound: !label,
}; };
}); });
@@ -618,27 +597,17 @@ export class HaTargetPickerItemRow extends LitElement {
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
} }
.error {
background: var(--ha-color-fill-warning-quiet-resting);
}
.error [slot="supporting-text"] {
color: var(--ha-color-on-warning-normal);
}
state-badge { state-badge {
color: var(--ha-color-on-neutral-quiet); color: var(--ha-color-on-neutral-quiet);
} }
.icon { .icon {
width: 24px;
display: flex; display: flex;
} }
img { img {
width: 24px; width: 24px;
height: 24px; height: 24px;
z-index: 1;
} }
ha-icon-button { ha-icon-button {
--mdc-icon-button-size: 32px; --mdc-icon-button-size: 32px;

View File

@@ -705,7 +705,7 @@ export class HaTargetPickerSelector extends LitElement {
) as EntityComboBoxItem[]; ) as EntityComboBoxItem[];
} }
if (!filterType && entities.length) { if (!filterType) {
// show group title // show group title
items.push( items.push(
this.hass.localize("ui.components.target-picker.type.entities") this.hass.localize("ui.components.target-picker.type.entities")
@@ -733,7 +733,7 @@ export class HaTargetPickerSelector extends LitElement {
devices = this._filterGroup("device", devices); devices = this._filterGroup("device", devices);
} }
if (!filterType && devices.length) { if (!filterType) {
// show group title // show group title
items.push( items.push(
this.hass.localize("ui.components.target-picker.type.devices") this.hass.localize("ui.components.target-picker.type.devices")
@@ -769,7 +769,7 @@ export class HaTargetPickerSelector extends LitElement {
) as FloorComboBoxItem[]; ) as FloorComboBoxItem[];
} }
if (!filterType && areasAndFloors.length) { if (!filterType) {
// show group title // show group title
items.push( items.push(
this.hass.localize("ui.components.target-picker.type.areas") this.hass.localize("ui.components.target-picker.type.areas")
@@ -811,7 +811,7 @@ export class HaTargetPickerSelector extends LitElement {
labels = this._filterGroup("label", labels); labels = this._filterGroup("label", labels);
} }
if (!filterType && labels.length) { if (!filterType) {
// show group title // show group title
items.push( items.push(
this.hass.localize("ui.components.target-picker.type.labels") this.hass.localize("ui.components.target-picker.type.labels")

View File

@@ -16,10 +16,14 @@ import memoizeOne from "memoize-one";
import { computeCssColor } from "../../common/color/compute-color"; import { computeCssColor } from "../../common/color/compute-color";
import { hex2rgb } from "../../common/color/convert-color"; import { hex2rgb } from "../../common/color/convert-color";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { slugify } from "../../common/string/slugify"; import { slugify } from "../../common/string/slugify";
import {
computeDeviceName,
computeDeviceNameDisplay,
} from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { getConfigEntry } from "../../data/config_entries"; import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context"; import { labelsContext } from "../../data/context";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
@@ -168,10 +172,23 @@ export class HaTargetPickerValueChip extends LitElement {
if (type === "entity") { if (type === "entity") {
this._setDomainName(computeDomain(itemId)); this._setDomainName(computeDomain(itemId));
const stateObj = this.hass.states[itemId]; const stateObject = this.hass.states[itemId];
const entityName = computeEntityName(
stateObject,
this.hass.entities,
this.hass.devices
);
const { device } = getEntityContext(
stateObject,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const deviceName = device ? computeDeviceName(device) : undefined;
return { return {
name: computeStateName(stateObj) || itemId, name: entityName || deviceName || itemId,
stateObject: stateObj, stateObject,
}; };
} }

View File

@@ -1,5 +1,3 @@
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { atLeastVersion } from "../common/config/version";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
export interface LogProvider { export interface LogProvider {
@@ -10,8 +8,4 @@ export interface LogProvider {
export const fetchErrorLog = (hass: HomeAssistant) => export const fetchErrorLog = (hass: HomeAssistant) =>
hass.callApi<string>("GET", "error_log"); hass.callApi<string>("GET", "error_log");
export const getErrorLogDownloadUrl = (hass: HomeAssistant) => export const getErrorLogDownloadUrl = "/api/error_log";
isComponentLoaded(hass, "hassio") &&
atLeastVersion(hass.config.version, 2025, 10)
? "/api/hassio/core/logs/latest"
: "/api/error_log";

View File

@@ -435,9 +435,9 @@ export const convertStatisticsToHistory = (
Object.entries(orderedStatistics).forEach(([key, value]) => { Object.entries(orderedStatistics).forEach(([key, value]) => {
const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({ const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
s: e.mean != null ? e.mean.toString() : e.state!.toString(), s: e.mean != null ? e.mean.toString() : e.state!.toString(),
lc: e.end / 1000, lc: e.start / 1000,
a: {}, a: {},
lu: e.end / 1000, lu: e.start / 1000,
})); }));
statsHistoryStates[key] = entityHistoryStates; statsHistoryStates[key] = entityHistoryStates;
}); });

View File

@@ -12,7 +12,6 @@ import {
mdiChatSleep, mdiChatSleep,
mdiClipboardList, mdiClipboardList,
mdiClock, mdiClock,
mdiCodeBraces,
mdiCog, mdiCog,
mdiCommentAlert, mdiCommentAlert,
mdiCounter, mdiCounter,
@@ -114,7 +113,6 @@ export const FALLBACK_DOMAIN_ICONS = {
text: mdiFormTextbox, text: mdiFormTextbox,
time: mdiClock, time: mdiClock,
timer: mdiTimerOutline, timer: mdiTimerOutline,
template: mdiCodeBraces,
todo: mdiClipboardList, todo: mdiClipboardList,
tts: mdiSpeakerMessage, tts: mdiSpeakerMessage,
vacuum: mdiRobotVacuum, vacuum: mdiRobotVacuum,

View File

@@ -264,7 +264,6 @@ export const getLabels = (
const items = outputLabels.map<PickerComboBoxItem>((label) => ({ const items = outputLabels.map<PickerComboBoxItem>((label) => ({
id: label.label_id, id: label.label_id,
primary: label.name, primary: label.name,
secondary: label.description ?? "",
icon: label.icon || undefined, icon: label.icon || undefined,
icon_path: label.icon ? undefined : mdiLabel, icon_path: label.icon ? undefined : mdiLabel,
sorting_label: label.name, sorting_label: label.name,

View File

@@ -1,4 +1,3 @@
import type { MediaSelectorValue } from "../../selector";
import type { LovelaceBadgeConfig } from "./badge"; import type { LovelaceBadgeConfig } from "./badge";
import type { LovelaceCardConfig } from "./card"; import type { LovelaceCardConfig } from "./card";
import type { LovelaceSectionRawConfig } from "./section"; import type { LovelaceSectionRawConfig } from "./section";
@@ -9,7 +8,7 @@ export interface ShowViewConfig {
} }
export interface LovelaceViewBackgroundConfig { export interface LovelaceViewBackgroundConfig {
image?: string | MediaSelectorValue; image?: string;
opacity?: number; opacity?: number;
size?: "auto" | "cover" | "contain"; size?: "auto" | "cover" | "contain";
alignment?: alignment?:

View File

@@ -222,7 +222,6 @@ export interface StopAction extends BaseAction {
export interface SequenceAction extends BaseAction { export interface SequenceAction extends BaseAction {
sequence: (ManualScriptConfig | Action)[]; sequence: (ManualScriptConfig | Action)[];
metadata?: {};
} }
export interface ParallelAction extends BaseAction { export interface ParallelAction extends BaseAction {
@@ -480,7 +479,6 @@ export const migrateAutomationAction = (
} }
if (typeof action === "object" && action !== null && "sequence" in action) { if (typeof action === "object" && action !== null && "sequence" in action) {
delete (action as SequenceAction).metadata;
for (const sequenceAction of (action as SequenceAction).sequence) { for (const sequenceAction of (action as SequenceAction).sequence) {
migrateAutomationAction(sequenceAction); migrateAutomationAction(sequenceAction);
} }

View File

@@ -5,6 +5,7 @@ import type {
import { ensureArray } from "../common/array/ensure-array"; import { ensureArray } from "../common/array/ensure-array";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature"; import { supportsFeature } from "../common/entity/supports-feature";
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { isHelperDomain } from "../panels/config/helpers/const"; import { isHelperDomain } from "../panels/config/helpers/const";
import type { UiAction } from "../panels/lovelace/components/hui-action-editor"; import type { UiAction } from "../panels/lovelace/components/hui-action-editor";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@@ -46,6 +47,8 @@ export type Selector =
| FileSelector | FileSelector
| IconSelector | IconSelector
| LabelSelector | LabelSelector
| ImageSelector
| BackgroundSelector
| LanguageSelector | LanguageSelector
| LocationSelector | LocationSelector
| MediaSelector | MediaSelector
@@ -270,6 +273,14 @@ export interface IconSelector {
} | null; } | null;
} }
export interface ImageSelector {
image: { original?: boolean; crop?: CropOptions } | null;
}
export interface BackgroundSelector {
background: { original?: boolean; crop?: CropOptions } | null;
}
export interface LabelSelector { export interface LabelSelector {
label: { label: {
multiple?: boolean; multiple?: boolean;

View File

@@ -484,7 +484,7 @@ class DataEntryFlowDialog extends LitElement {
this._unsubDataEntryFlowProgress = undefined; this._unsubDataEntryFlowProgress = undefined;
} }
if (_step.next_flow[0] === "config_flow") { if (_step.next_flow[0] === "config_flow") {
showConfigFlowDialog(this, { showConfigFlowDialog(this._params!.dialogParentElement!, {
continueFlowId: _step.next_flow[1], continueFlowId: _step.next_flow[1],
carryOverDevices: this._devices( carryOverDevices: this._devices(
this._params!.flowConfig.showDevices, this._params!.flowConfig.showDevices,
@@ -496,23 +496,32 @@ class DataEntryFlowDialog extends LitElement {
}); });
} else if (_step.next_flow[0] === "options_flow") { } else if (_step.next_flow[0] === "options_flow") {
if (_step.type === "create_entry") { if (_step.type === "create_entry") {
showOptionsFlowDialog(this, _step.result!, { showOptionsFlowDialog(
this._params!.dialogParentElement!,
_step.result!,
{
continueFlowId: _step.next_flow[1], continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult, navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback, dialogClosedCallback: this._params!.dialogClosedCallback,
}); }
);
} }
} else if (_step.next_flow[0] === "config_subentries_flow") { } else if (_step.next_flow[0] === "config_subentries_flow") {
if (_step.type === "create_entry") { if (_step.type === "create_entry") {
showSubConfigFlowDialog(this, _step.result!, _step.next_flow[0], { showSubConfigFlowDialog(
this._params!.dialogParentElement!,
_step.result!,
_step.next_flow[0],
{
continueFlowId: _step.next_flow[1], continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult, navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback, dialogClosedCallback: this._params!.dialogClosedCallback,
}); }
);
} }
} else { } else {
this.closeDialog(); this.closeDialog();
showAlertDialog(this, { showAlertDialog(this._params!.dialogParentElement!, {
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error", "ui.panel.config.integrations.config_flow.error",
{ error: `Unsupported next flow type: ${_step.next_flow[0]}` } { error: `Unsupported next flow type: ${_step.next_flow[0]}` }

View File

@@ -1,152 +0,0 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-alert";
import "../../components/ha-icon";
import "../../components/ha-list-item";
import "../../components/ha-spinner";
import type {
ExternalEntityAddToActions,
ExternalEntityAddToAction,
} from "../../external_app/external_messaging";
import { showToast } from "../../util/toast";
import type { HomeAssistant } from "../../types";
@customElement("ha-more-info-add-to")
export class HaMoreInfoAddTo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string;
@state() private _externalActions?: ExternalEntityAddToActions = {
actions: [],
};
@state() private _loading = true;
private async _loadExternalActions() {
if (this.hass.auth.external?.config.hasEntityAddTo) {
this._externalActions =
await this.hass.auth.external?.sendMessage<"entity/add_to/get_actions">(
{
type: "entity/add_to/get_actions",
payload: { entity_id: this.entityId },
}
);
}
}
private async _actionSelected(ev: CustomEvent) {
const action = (ev.currentTarget as any)
.action as ExternalEntityAddToAction;
if (!action.enabled) {
return;
}
try {
await this.hass.auth.external!.fireMessage({
type: "entity/add_to",
payload: {
entity_id: this.entityId,
app_payload: action.app_payload,
},
});
} catch (err: any) {
showToast(this, {
message: this.hass.localize(
"ui.dialogs.more_info_control.add_to.action_failed",
{
error: err.message || err,
}
),
});
}
}
protected async firstUpdated() {
await this._loadExternalActions();
this._loading = false;
}
protected render() {
if (this._loading) {
return html`
<div class="loading">
<ha-spinner></ha-spinner>
</div>
`;
}
if (!this._externalActions?.actions.length) {
return html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.dialogs.more_info_control.add_to.no_actions"
)}
</ha-alert>
`;
}
return html`
<div class="actions-list">
${this._externalActions.actions.map(
(action) => html`
<ha-list-item
graphic="icon"
.disabled=${!action.enabled}
.action=${action}
.twoline=${!!action.details}
@click=${this._actionSelected}
>
<span>${action.name}</span>
${action.details
? html`<span slot="secondary">${action.details}</span>`
: nothing}
<ha-icon slot="graphic" .icon=${action.mdi_icon}></ha-icon>
</ha-list-item>
`
)}
</div>
`;
}
static styles = css`
:host {
display: block;
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6)
var(--ha-space-6);
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: var(--ha-space-8);
}
.actions-list {
display: flex;
flex-direction: column;
}
ha-list-item {
cursor: pointer;
}
ha-list-item[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
ha-icon {
display: flex;
align-items: center;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-add-to": HaMoreInfoAddTo;
}
}

View File

@@ -8,7 +8,6 @@ import {
mdiPencil, mdiPencil,
mdiPencilOff, mdiPencilOff,
mdiPencilOutline, mdiPencilOutline,
mdiPlusBoxMultipleOutline,
mdiTransitConnectionVariant, mdiTransitConnectionVariant,
} from "@mdi/js"; } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
@@ -61,7 +60,6 @@ import {
computeShowLogBookComponent, computeShowLogBookComponent,
} from "./const"; } from "./const";
import "./controls/more-info-default"; import "./controls/more-info-default";
import "./ha-more-info-add-to";
import "./ha-more-info-history-and-logbook"; import "./ha-more-info-history-and-logbook";
import "./ha-more-info-info"; import "./ha-more-info-info";
import "./ha-more-info-settings"; import "./ha-more-info-settings";
@@ -75,7 +73,7 @@ export interface MoreInfoDialogParams {
data?: Record<string, any>; data?: Record<string, any>;
} }
type View = "info" | "history" | "settings" | "related" | "add_to"; type View = "info" | "history" | "settings" | "related";
interface ChildView { interface ChildView {
viewTag: string; viewTag: string;
@@ -196,10 +194,6 @@ export class MoreInfoDialog extends LitElement {
); );
} }
private _shouldShowAddEntityTo(): boolean {
return !!this.hass.auth.external?.config.hasEntityAddTo;
}
private _getDeviceId(): string | null { private _getDeviceId(): string | null {
const entity = this.hass.entities[this._entityId!] as const entity = this.hass.entities[this._entityId!] as
| EntityRegistryEntry | EntityRegistryEntry
@@ -301,11 +295,6 @@ export class MoreInfoDialog extends LitElement {
this._setView("related"); this._setView("related");
} }
private _goToAddEntityTo(ev) {
if (!shouldHandleRequestSelectedEvent(ev)) return;
this._setView("add_to");
}
private _breadcrumbClick(ev: Event) { private _breadcrumbClick(ev: Event) {
ev.stopPropagation(); ev.stopPropagation();
this._setView("related"); this._setView("related");
@@ -532,22 +521,6 @@ export class MoreInfoDialog extends LitElement {
.path=${mdiInformationOutline} .path=${mdiInformationOutline}
></ha-svg-icon> ></ha-svg-icon>
</ha-list-item> </ha-list-item>
${this._shouldShowAddEntityTo()
? html`
<ha-list-item
graphic="icon"
@request-selected=${this._goToAddEntityTo}
>
${this.hass.localize(
"ui.dialogs.more_info_control.add_entity_to"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiPlusBoxMultipleOutline}
></ha-svg-icon>
</ha-list-item>
`
: nothing}
</ha-button-menu> </ha-button-menu>
` `
: nothing} : nothing}
@@ -640,13 +613,6 @@ export class MoreInfoDialog extends LitElement {
: "entity"} : "entity"}
></ha-related-items> ></ha-related-items>
` `
: this._currView === "add_to"
? html`
<ha-more-info-add-to
.hass=${this.hass}
.entityId=${entityId}
></ha-more-info-add-to>
`
: nothing : nothing
)} )}
</div> </div>
@@ -712,8 +678,8 @@ export class MoreInfoDialog extends LitElement {
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */ /* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
--vertical-align-dialog: flex-start; --vertical-align-dialog: flex-start;
--dialog-surface-margin-top: max( --dialog-surface-margin-top: max(
var(--ha-space-10), 40px,
var(--safe-area-inset-top, var(--ha-space-0)) var(--safe-area-inset-top, 0px)
); );
--dialog-content-padding: 0; --dialog-content-padding: 0;
} }
@@ -732,15 +698,14 @@ export class MoreInfoDialog extends LitElement {
} }
ha-more-info-history-and-logbook { ha-more-info-history-and-logbook {
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6) padding: 8px 24px 24px 24px;
var(--ha-space-6);
display: block; display: block;
} }
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
/* When in fullscreen dialog should be attached to top */ /* When in fullscreen dialog should be attached to top */
ha-dialog { ha-dialog {
--dialog-surface-margin-top: var(--ha-space-0); --dialog-surface-margin-top: 0px;
} }
} }
@@ -765,8 +730,7 @@ export class MoreInfoDialog extends LitElement {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
margin: var(--ha-space-0) var(--ha-space-0) margin: 0 0 -10px 0;
calc(var(--ha-space-2) * -1) var(--ha-space-0);
} }
.title p { .title p {
@@ -788,9 +752,9 @@ export class MoreInfoDialog extends LitElement {
font-size: var(--ha-font-size-m); font-size: var(--ha-font-size-m);
line-height: 16px; line-height: 16px;
--mdc-icon-size: 16px; --mdc-icon-size: 16px;
padding: var(--ha-space-1); padding: 4px;
margin: calc(var(--ha-space-1) * -1); margin: -4px;
margin-top: calc(var(--ha-space-2) * -1); margin-top: -10px;
background: none; background: none;
border: none; border: none;
outline: none; outline: none;

View File

@@ -152,18 +152,10 @@ export class MoreInfoHistory extends LitElement {
} }
} }
private _setUpdateTimer() { private _setRedrawTimer() {
// redraw the graph every minute to update the time axis
clearInterval(this._interval); clearInterval(this._interval);
this._interval = window.setInterval(() => { this._interval = window.setInterval(() => this._redrawGraph(), 1000 * 60);
// If using statistics, refresh the data
if (this._statistics) {
this._fetchStatistics();
}
// If using history, redraw the graph to update the time axis
if (this._stateHistory) {
this._redrawGraph();
}
}, 1000 * 60);
} }
private async _getStatisticsMetaData(statisticIds: string[] | undefined) { private async _getStatisticsMetaData(statisticIds: string[] | undefined) {
@@ -178,7 +170,16 @@ export class MoreInfoHistory extends LitElement {
return statisticsMetaData; return statisticsMetaData;
} }
private async _fetchStatistics(): Promise<boolean> { private async _getStateHistory(): Promise<void> {
if (
isComponentLoaded(this.hass, "recorder") &&
computeDomain(this.entityId) === "sensor"
) {
const stateObj = this.hass.states[this.entityId];
// If there is no state class, the integration providing the entity
// has not opted into statistics so there is no need to check as it
// requires another round-trip to the server.
if (stateObj && stateObj.attributes.state_class) {
// Fire off the metadata and fetch at the same time // Fire off the metadata and fetch at the same time
// to avoid waiting in sequence so the UI responds // to avoid waiting in sequence so the UI responds
// faster. // faster.
@@ -192,30 +193,14 @@ export class MoreInfoHistory extends LitElement {
undefined, undefined,
statTypes statTypes
); );
const [metadata, statistics] = await Promise.all([_metadata, _statistics]); const [metadata, statistics] = await Promise.all([
_metadata,
_statistics,
]);
if (metadata && Object.keys(metadata).length) { if (metadata && Object.keys(metadata).length) {
this._metadata = metadata; this._metadata = metadata;
this._statistics = statistics; this._statistics = statistics;
this._statNames = { [this.entityId]: "" }; this._statNames = { [this.entityId]: "" };
return true;
}
return false;
}
private async _getStateHistory(): Promise<void> {
if (
isComponentLoaded(this.hass, "recorder") &&
computeDomain(this.entityId) === "sensor"
) {
const stateObj = this.hass.states[this.entityId];
// If there is no state class, the integration providing the entity
// has not opted into statistics so there is no need to check as it
// requires another round-trip to the server.
if (stateObj && stateObj.attributes.state_class) {
const hasStatistics = await this._fetchStatistics();
if (hasStatistics) {
// Using statistics, set up refresh timer
this._setUpdateTimer();
return; return;
} }
} }
@@ -253,7 +238,7 @@ export class MoreInfoHistory extends LitElement {
this._error = err; this._error = err;
return undefined; return undefined;
}); });
this._setUpdateTimer(); this._setRedrawTimer();
} }
static styles = [ static styles = [

View File

@@ -1011,8 +1011,8 @@ export class QuickBar extends LitElement {
--mdc-dialog-max-width: 800px; --mdc-dialog-max-width: 800px;
--mdc-dialog-min-width: 500px; --mdc-dialog-min-width: 500px;
--dialog-surface-position: fixed; --dialog-surface-position: fixed;
--dialog-surface-top: var(--ha-space-10); --dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100% - var(--ha-space-18)); --mdc-dialog-max-height: calc(100% - 72px);
} }
} }
@@ -1055,8 +1055,8 @@ export class QuickBar extends LitElement {
} }
span.command-text { span.command-text {
margin-left: var(--ha-space-2); margin-left: 8px;
margin-inline-start: var(--ha-space-2); margin-inline-start: 8px;
margin-inline-end: initial; margin-inline-end: initial;
direction: var(--direction); direction: var(--direction);
} }
@@ -1069,8 +1069,8 @@ export class QuickBar extends LitElement {
ha-md-list-item.two-line { ha-md-list-item.two-line {
--md-list-item-one-line-container-height: 64px; --md-list-item-one-line-container-height: 64px;
--md-list-item-two-line-container-height: 64px; --md-list-item-two-line-container-height: 64px;
--md-list-item-top-space: var(--ha-space-2); --md-list-item-top-space: 8px;
--md-list-item-bottom-space: var(--ha-space-2); --md-list-item-bottom-space: 8px;
} }
ha-md-list-item.three-line { ha-md-list-item.three-line {
@@ -1078,8 +1078,8 @@ export class QuickBar extends LitElement {
--md-list-item-one-line-container-height: 72px; --md-list-item-one-line-container-height: 72px;
--md-list-item-two-line-container-height: 72px; --md-list-item-two-line-container-height: 72px;
--md-list-item-three-line-container-height: 72px; --md-list-item-three-line-container-height: 72px;
--md-list-item-top-space: var(--ha-space-2); --md-list-item-top-space: 8px;
--md-list-item-bottom-space: var(--ha-space-2); --md-list-item-bottom-space: 8px;
} }
ha-md-list-item .code { ha-md-list-item .code {
@@ -1104,11 +1104,11 @@ export class QuickBar extends LitElement {
} }
ha-tip { ha-tip {
padding: var(--ha-space-5); padding: 20px;
} }
.nothing-found { .nothing-found {
padding: var(--ha-space-4) var(--ha-space-0); padding: 16px 0px;
text-align: center; text-align: center;
} }

View File

@@ -102,17 +102,6 @@ class DialogEditSidebar extends LitElement {
this.hass.locale this.hass.locale
); );
// Add default hidden panels that are missing in hidden
for (const panel of panels) {
if (
!panel.default_visible &&
!this._order.includes(panel.url_path) &&
!this._hidden.includes(panel.url_path)
) {
this._hidden.push(panel.url_path);
}
}
const items = [ const items = [
...beforeSpacer, ...beforeSpacer,
...panels.filter((panel) => this._hidden!.includes(panel.url_path)), ...panels.filter((panel) => this._hidden!.includes(panel.url_path)),

View File

@@ -193,12 +193,12 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
).map( ).map(
(lang) => (lang) =>
html`<ha-md-menu-item html`<ha-md-menu-item
.value=${lang.id} .value=${lang.value}
@click=${this._handlePickLanguage} @click=${this._handlePickLanguage}
@keydown=${this._handlePickLanguage} @keydown=${this._handlePickLanguage}
.selected=${this._language === lang.id} .selected=${this._language === lang.value}
> >
${lang.primary} ${lang.label}
</ha-md-menu-item>` </ha-md-menu-item>`
)} )}
</ha-md-button-menu>` </ha-md-button-menu>`

View File

@@ -36,13 +36,6 @@ interface EMOutgoingMessageConfigGet extends EMMessage {
type: "config/get"; type: "config/get";
} }
interface EMOutgoingMessageEntityAddToGetActions extends EMMessage {
type: "entity/add_to/get_actions";
payload: {
entity_id: string;
};
}
interface EMOutgoingMessageBarCodeScan extends EMMessage { interface EMOutgoingMessageBarCodeScan extends EMMessage {
type: "bar_code/scan"; type: "bar_code/scan";
payload: { payload: {
@@ -82,10 +75,6 @@ interface EMOutgoingMessageWithAnswer {
request: EMOutgoingMessageConfigGet; request: EMOutgoingMessageConfigGet;
response: ExternalConfig; response: ExternalConfig;
}; };
"entity/add_to/get_actions": {
request: EMOutgoingMessageEntityAddToGetActions;
response: ExternalEntityAddToActions;
};
} }
interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage { interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage {
@@ -168,14 +157,6 @@ interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
}; };
} }
interface EMOutgoingMessageAddEntityTo extends EMMessage {
type: "entity/add_to";
payload: {
entity_id: string;
app_payload: string; // Opaque string received from get_actions
};
}
type EMOutgoingMessageWithoutAnswer = type EMOutgoingMessageWithoutAnswer =
| EMMessageResultError | EMMessageResultError
| EMMessageResultSuccess | EMMessageResultSuccess
@@ -196,8 +177,7 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageThemeUpdate | EMOutgoingMessageThemeUpdate
| EMOutgoingMessageThreadStoreInPlatformKeychain | EMOutgoingMessageThreadStoreInPlatformKeychain
| EMOutgoingMessageImprovScan | EMOutgoingMessageImprovScan
| EMOutgoingMessageImprovConfigureDevice | EMOutgoingMessageImprovConfigureDevice;
| EMOutgoingMessageAddEntityTo;
export interface EMIncomingMessageRestart { export interface EMIncomingMessageRestart {
id: number; id: number;
@@ -313,31 +293,18 @@ type EMIncomingMessage =
type EMIncomingMessageHandler = (msg: EMIncomingMessageCommands) => boolean; type EMIncomingMessageHandler = (msg: EMIncomingMessageCommands) => boolean;
export interface ExternalConfig { export interface ExternalConfig {
hasSettingsScreen?: boolean; hasSettingsScreen: boolean;
hasSidebar?: boolean; hasSidebar: boolean;
canWriteTag?: boolean; canWriteTag: boolean;
hasExoPlayer?: boolean; hasExoPlayer: boolean;
canCommissionMatter?: boolean; canCommissionMatter: boolean;
canImportThreadCredentials?: boolean; canImportThreadCredentials: boolean;
canTransferThreadCredentialsToKeychain?: boolean; canTransferThreadCredentialsToKeychain: boolean;
hasAssist?: boolean; hasAssist: boolean;
hasBarCodeScanner?: number; hasBarCodeScanner: number;
canSetupImprov?: boolean; canSetupImprov: boolean;
downloadFileSupported?: boolean; downloadFileSupported: boolean;
appVersion?: string; appVersion: string;
hasEntityAddTo?: boolean; // Supports "Add to" from more-info dialog, with action coming from external app
}
export interface ExternalEntityAddToAction {
enabled: boolean;
name: string; // Translated name of the action to be displayed in the UI
details?: string; // Optional translated details of the action to be displayed in the UI
mdi_icon: string; // MDI icon name to be displayed in the UI (e.g., "mdi:car")
app_payload: string; // Opaque string to be sent back when the action is selected
}
export interface ExternalEntityAddToActions {
actions: ExternalEntityAddToAction[];
} }
export class ExternalMessaging { export class ExternalMessaging {

View File

@@ -97,9 +97,6 @@ export const ENTITY_COMPONENT_ICONS: Record<string, ComponentIcons> = {
pm25: { pm25: {
default: "mdi:molecule", default: "mdi:molecule",
}, },
pm4: {
default: "mdi:molecule",
},
power: { power: {
default: "mdi:flash", default: "mdi:flash",
}, },
@@ -677,9 +674,6 @@ export const ENTITY_COMPONENT_ICONS: Record<string, ComponentIcons> = {
pm25: { pm25: {
default: "mdi:molecule", default: "mdi:molecule",
}, },
pm4: {
default: "mdi:molecule",
},
power: { power: {
default: "mdi:flash", default: "mdi:flash",
}, },

View File

@@ -33,7 +33,7 @@ const COMPONENTS = {
"media-browser": () => "media-browser": () =>
import("../panels/media-browser/ha-panel-media-browser"), import("../panels/media-browser/ha-panel-media-browser"),
light: () => import("../panels/light/ha-panel-light"), light: () => import("../panels/light/ha-panel-light"),
security: () => import("../panels/security/ha-panel-security"), safety: () => import("../panels/safety/ha-panel-safety"),
climate: () => import("../panels/climate/ha-panel-climate"), climate: () => import("../panels/climate/ha-panel-climate"),
}; };

View File

@@ -1,104 +0,0 @@
import type { ReactiveElement } from "lit";
import { listenMediaQuery } from "../common/dom/media_query";
import type { HomeAssistant } from "../types";
import type { Condition } from "../panels/lovelace/common/validate-condition";
import { checkConditionsMet } from "../panels/lovelace/common/validate-condition";
type Constructor<T> = abstract new (...args: any[]) => T;
/**
* Extract media queries from conditions recursively
*/
export function extractMediaQueries(conditions: Condition[]): string[] {
return conditions.reduce<string[]>((array, c) => {
if ("conditions" in c && c.conditions) {
array.push(...extractMediaQueries(c.conditions));
}
if (c.condition === "screen" && c.media_query) {
array.push(c.media_query);
}
return array;
}, []);
}
/**
* Helper to setup media query listeners for conditional visibility
*/
export function setupMediaQueryListeners(
conditions: Condition[],
hass: HomeAssistant,
addListener: (unsub: () => void) => void,
onUpdate: (conditionsMet: boolean) => void
): void {
const mediaQueries = extractMediaQueries(conditions);
if (mediaQueries.length === 0) return;
// Optimization for single media query
const hasOnlyMediaQuery =
conditions.length === 1 &&
conditions[0].condition === "screen" &&
!!conditions[0].media_query;
mediaQueries.forEach((mediaQuery) => {
const unsub = listenMediaQuery(mediaQuery, (matches) => {
if (hasOnlyMediaQuery) {
onUpdate(matches);
} else {
const conditionsMet = checkConditionsMet(conditions, hass);
onUpdate(conditionsMet);
}
});
addListener(unsub);
});
}
/**
* Mixin to handle conditional listeners for visibility control
*
* Provides lifecycle management for listeners (media queries, time-based, state changes, etc.)
* that control conditional visibility of components.
*
* Usage:
* 1. Extend your component with ConditionalListenerMixin(ReactiveElement)
* 2. Override setupConditionalListeners() to setup your listeners
* 3. Use addConditionalListener() to register unsubscribe functions
* 4. Call clearConditionalListeners() and setupConditionalListeners() when config changes
*
* The mixin automatically:
* - Sets up listeners when component connects to DOM
* - Cleans up listeners when component disconnects from DOM
*/
export const ConditionalListenerMixin = <
T extends Constructor<ReactiveElement>,
>(
superClass: T
) => {
abstract class ConditionalListenerClass extends superClass {
private __listeners: (() => void)[] = [];
public connectedCallback() {
super.connectedCallback();
this.setupConditionalListeners();
}
public disconnectedCallback() {
super.disconnectedCallback();
this.clearConditionalListeners();
}
protected clearConditionalListeners(): void {
this.__listeners.forEach((unsub) => unsub());
this.__listeners = [];
}
protected addConditionalListener(unsubscribe: () => void): void {
this.__listeners.push(unsubscribe);
}
protected setupConditionalListeners(): void {
// Override in subclass
}
}
return ConditionalListenerClass;
};

View File

@@ -143,14 +143,9 @@ class DialogCalendarEventDetail extends LitElement {
this.hass.locale.time_zone, this.hass.locale.time_zone,
this.hass.config.time_zone this.hass.config.time_zone
); );
// For all-day events (date-only strings), parse without timezone to avoid offset issues const start = new TZDate(this._data!.dtstart, timeZone);
const start = isDate(this._data!.dtstart) const endValue = new TZDate(this._data!.dtend, timeZone);
? new Date(this._data!.dtstart + "T00:00:00") // All day events should be displayed as a day earlier
: new TZDate(this._data!.dtstart, timeZone);
const endValue = isDate(this._data!.dtend)
? new Date(this._data!.dtend + "T00:00:00")
: new TZDate(this._data!.dtend, timeZone);
// All day event end dates are exclusive in iCalendar format, subtract one day for display
const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue; const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue;
// The range can be shortened when the start and end are on the same day. // The range can be shortened when the start and end are on the same day.
if (isSameDay(start, end)) { if (isSameDay(start, end)) {

View File

@@ -332,15 +332,6 @@ class DialogCalendarEventEditor extends LitElement {
private _allDayToggleChanged(ev) { private _allDayToggleChanged(ev) {
this._allDay = ev.target.checked; this._allDay = ev.target.checked;
// When switching to all-day mode, normalize dates to midnight so time portions don't interfere with date comparisons
if (this._allDay && this._dtstart && this._dtend) {
this._dtstart = new Date(
formatDate(this._dtstart, this._timeZone!) + "T00:00:00"
);
this._dtend = new Date(
formatDate(this._dtend, this._timeZone!) + "T00:00:00"
);
}
} }
private _startDateChanged(ev: CustomEvent) { private _startDateChanged(ev: CustomEvent) {

View File

@@ -1,23 +1,24 @@
import type { CSSResultGroup, PropertyValues } from "lit"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { goBack } from "../../common/navigate"; import { goBack } from "../../common/navigate";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-icon-button-arrow-prev"; import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button"; import "../../components/ha-menu-button";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view"; import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types"; import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view"; import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container"; import "../lovelace/views/hui-view-container";
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = { const CLIMATE_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: { strategy: {
type: "climate", type: "climate",
}, },
},
],
}; };
@customElement("ha-panel-climate") @customElement("ha-panel-climate")
@@ -32,68 +33,33 @@ class PanelClimate extends LitElement {
@state() private _searchParms = new URLSearchParams(window.location.search); @state() private _searchParms = new URLSearchParams(window.location.search);
public willUpdate(changedProps: PropertyValues) { public firstUpdated(_changedProperties: PropertyValues): void {
super.willUpdate(changedProps); super.firstUpdated(_changedProperties);
// Initial setup
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
this._setLovelace();
return;
} }
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
}
if (!changedProps.has("hass")) { if (!changedProps.has("hass")) {
return; return;
} }
const oldHass = changedProps.get("hass") as this["hass"]; const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && oldHass.localize !== this.hass.localize) { if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
return;
}
if (oldHass && this.hass) {
// If the entity registry changed, ask the user if they want to refresh the config
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
return;
}
}
// If ha started, refresh the config
if (
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
) {
this._setLovelace(); this._setLovelace();
} }
} }
}
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
200
);
private _registriesChanged = async () => {
this._setLovelace();
};
private _back(ev) { private _back(ev) {
ev.stopPropagation(); ev.stopPropagation();
goBack(); goBack();
} }
protected render() { protected render(): TemplateResult {
return html` return html`
<div class="header"> <div class="header">
<div class="toolbar"> <div class="toolbar">
${ ${this._searchParms.has("historyBack")
this._searchParms.has("historyBack")
? html` ? html`
<ha-icon-button-arrow-prev <ha-icon-button-arrow-prev
@click=${this._back} @click=${this._back}
@@ -106,45 +72,26 @@ class PanelClimate extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-menu-button> ></ha-menu-button>
` `}
}
<div class="main-title">${this.hass.localize("panel.climate")}</div> <div class="main-title">${this.hass.localize("panel.climate")}</div>
</div> </div>
</div> </div>
${
this._lovelace
? html`
<hui-view-container .hass=${this.hass}> <hui-view-container .hass=${this.hass}>
<hui-view <hui-view
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.lovelace=${this._lovelace} .lovelace=${this._lovelace}
.index=${this._viewIndex} .index=${this._viewIndex}
></hui-view ></hui-view>
></hui-view-container>
`
: nothing
}
</hui-view-container> </hui-view-container>
`; `;
} }
private async _setLovelace() { private _setLovelace() {
const viewConfig = await generateLovelaceViewStrategy(
CLIMATE_LOVELACE_VIEW_CONFIG,
this.hass
);
const config = { views: [viewConfig] };
const rawConfig = { views: [CLIMATE_LOVELACE_VIEW_CONFIG] };
if (deepEqual(config, this._lovelace?.config)) {
return;
}
this._lovelace = { this._lovelace = {
config: config, config: CLIMATE_LOVELACE_CONFIG,
rawConfig: rawConfig, rawConfig: CLIMATE_LOVELACE_CONFIG,
editMode: false, editMode: false,
urlPath: "climate", urlPath: "climate",
mode: "generated", mode: "generated",

View File

@@ -15,7 +15,6 @@ import {
getFloors, getFloors,
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper"; } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure"; import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
export interface ClimateViewStrategyConfig { export interface ClimateViewStrategyConfig {
type: "climate"; type: "climate";
@@ -115,24 +114,6 @@ const processAreasForClimate = (
return cards; return cards;
}; };
const processUnassignedEntities = (
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const unassignedFilter = generateEntityFilter(hass, {
area: null,
});
const unassignedEntities = entities.filter(unassignedFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", true);
for (const entityId of unassignedEntities) {
areaCards.push(computeTileCard(entityId));
}
return areaCards;
};
@customElement("climate-view-strategy") @customElement("climate-view-strategy")
export class ClimateViewStrategy extends ReactiveElement { export class ClimateViewStrategy extends ReactiveElement {
static async generate( static async generate(
@@ -171,7 +152,6 @@ export class ClimateViewStrategy extends ReactiveElement {
floorCount > 1 floorCount > 1
? floor.name ? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"), : hass.localize("ui.panel.lovelace.strategy.home.areas"),
icon: floor.icon || floorDefaultIcon(floor),
}, },
], ],
}; };
@@ -208,33 +188,10 @@ export class ClimateViewStrategy extends ReactiveElement {
} }
} }
// Process unassigned entities
const unassignedCards = processUnassignedEntities(hass, entities);
if (unassignedCards.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
sections.length > 0
? hass.localize(
"ui.panel.lovelace.strategy.climate.other_devices"
)
: hass.localize("ui.panel.lovelace.strategy.climate.devices"),
},
...unassignedCards,
],
};
sections.push(section);
}
return { return {
type: "sections", type: "sections",
max_columns: 2, max_columns: 2,
sections: sections, sections: sections || [],
}; };
} }
} }

View File

@@ -265,8 +265,11 @@ class DialogAreaDetail extends LitElement {
${this.hass.localize("ui.common.delete")} ${this.hass.localize("ui.common.delete")}
</ha-button>` </ha-button>`
: nothing} : nothing}
<div slot="primaryAction">
<ha-button appearance="plain" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button <ha-button
slot="primaryAction"
@click=${this._updateEntry} @click=${this._updateEntry}
.disabled=${nameInvalid || !!this._submitting} .disabled=${nameInvalid || !!this._submitting}
> >
@@ -274,6 +277,7 @@ class DialogAreaDetail extends LitElement {
? this.hass.localize("ui.common.save") ? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.create")} : this.hass.localize("ui.common.create")}
</ha-button> </ha-button>
</div>
</ha-dialog> </ha-dialog>
`; `;
} }

View File

@@ -8,24 +8,24 @@ import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/chips/ha-chip-set"; import "../../../components/chips/ha-chip-set";
import "../../../components/chips/ha-input-chip"; import "../../../components/chips/ha-input-chip";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-aliases-editor";
import "../../../components/ha-area-picker";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-aliases-editor";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-icon-picker"; import "../../../components/ha-icon-picker";
import "../../../components/ha-picture-upload"; import "../../../components/ha-picture-upload";
import "../../../components/ha-settings-row"; import "../../../components/ha-settings-row";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import { updateAreaRegistryEntry } from "../../../data/area_registry"; import "../../../components/ha-area-picker";
import type { import type {
FloorRegistryEntry, FloorRegistryEntry,
FloorRegistryEntryMutableParams, FloorRegistryEntryMutableParams,
} from "../../../data/floor_registry"; } from "../../../data/floor_registry";
import { haStyle, haStyleDialog } from "../../../resources/styles"; import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { showAreaRegistryDetailDialog } from "./show-dialog-area-registry-detail";
import type { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail"; import type { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail";
import { showAreaRegistryDetailDialog } from "./show-dialog-area-registry-detail";
import { updateAreaRegistryEntry } from "../../../data/area_registry";
class DialogFloorDetail extends LitElement { class DialogFloorDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -168,6 +168,11 @@ class DialogFloorDetail extends LitElement {
)} )}
</h3> </h3>
<p class="description">
${this.hass.localize(
"ui.panel.config.floors.editor.areas_description"
)}
</p>
${areas.length ${areas.length
? html`<ha-chip-set> ? html`<ha-chip-set>
${repeat( ${repeat(
@@ -192,17 +197,13 @@ class DialogFloorDetail extends LitElement {
</ha-input-chip>` </ha-input-chip>`
)} )}
</ha-chip-set>` </ha-chip-set>`
: html`<p class="description"> : nothing}
${this.hass.localize(
"ui.panel.config.floors.editor.areas_description"
)}
</p>`}
<ha-area-picker <ha-area-picker
no-add no-add
.hass=${this.hass} .hass=${this.hass}
@value-changed=${this._addArea} @value-changed=${this._addArea}
.excludeAreas=${areas.map((a) => a.area_id)} .excludeAreas=${areas.map((a) => a.area_id)}
.addButtonLabel=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.floors.editor.add_area" "ui.panel.config.floors.editor.add_area"
)} )}
></ha-area-picker> ></ha-area-picker>

View File

@@ -588,11 +588,7 @@ export default class HaAutomationActionRow extends LitElement {
...this._clipboard, ...this._clipboard,
action: deepClone(this.action), action: deepClone(this.action),
}; };
let action = this.action; copyToClipboard(dump(this.action));
if ("sequence" in action) {
action = { ...this.action, metadata: {} };
}
copyToClipboard(dump(action));
} }
private _onDisable = () => { private _onDisable = () => {

View File

@@ -257,7 +257,7 @@ class DialogAddAutomationElement
const results = fuse.multiTermsSearch(filter); const results = fuse.multiTermsSearch(filter);
if (results) { if (results) {
return results.map((result) => result.item).filter((item) => item.name); return results.map((result) => result.item);
} }
return items; return items;
} }
@@ -294,7 +294,7 @@ class DialogAddAutomationElement
const results = fuse.multiTermsSearch(filter); const results = fuse.multiTermsSearch(filter);
if (results) { if (results) {
return results.map((result) => result.item).filter((item) => item.name); return results.map((result) => result.item);
} }
return items; return items;
} }
@@ -383,16 +383,9 @@ class DialogAddAutomationElement
generatedCollections.push({ generatedCollections.push({
titleKey: collection.titleKey, titleKey: collection.titleKey,
groups: groups.sort((a, b) => { groups: groups.sort((a, b) =>
// make sure device is always on top stringCompare(a.name, b.name, this.hass.locale.language)
if (a.key === "device" || a.key === "device_id") { ),
return -1;
}
if (b.key === "device" || b.key === "device_id") {
return 1;
}
return stringCompare(a.name, b.name, this.hass.locale.language);
}),
}); });
}); });
return generatedCollections; return generatedCollections;
@@ -685,7 +678,7 @@ class DialogAddAutomationElement
); );
const typeTitle = this.hass.localize( const typeTitle = this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.add` `ui.panel.config.automation.editor.${automationElementType}s.header`
); );
const tabButtons = [ const tabButtons = [
@@ -902,9 +895,7 @@ class DialogAddAutomationElement
return html` return html`
<div class="items-title ${this._itemsScrolled ? "scrolled" : ""}"> <div class="items-title ${this._itemsScrolled ? "scrolled" : ""}">
${this._tab === "blocks" && !this._filter ${title}
? this.hass.localize("ui.panel.config.automation.editor.blocks")
: title}
</div> </div>
<ha-md-list <ha-md-list
dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)} dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)}
@@ -1054,7 +1045,6 @@ class DialogAddAutomationElement
private _onSearchFocus(ev) { private _onSearchFocus(ev) {
this._removeKeyboardShortcuts = tinykeys(ev.target, { this._removeKeyboardShortcuts = tinykeys(ev.target, {
ArrowDown: this._focusSearchList, ArrowDown: this._focusSearchList,
Enter: this._pickSingleItem,
}); });
} }
@@ -1071,39 +1061,6 @@ class DialogAddAutomationElement
this._itemsListFirstElement.focus(); this._itemsListFirstElement.focus();
}; };
private _pickSingleItem = (ev: KeyboardEvent) => {
if (!this._filter) {
return;
}
ev.preventDefault();
const automationElementType = this._params!.type;
const items = [
...this._getFilteredItems(
automationElementType,
this._filter,
this.hass.localize,
this.hass.services,
this._manifests
),
...(automationElementType !== "trigger"
? this._getFilteredBuildingBlocks(
automationElementType,
this._filter,
this.hass.localize
)
: []),
];
if (items.length !== 1) {
return;
}
this._params!.add(items[0].key);
this.closeDialog();
};
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
css` css`
@@ -1180,11 +1137,11 @@ class DialogAddAutomationElement
} }
.groups .selected { .groups .selected {
background-color: var(--ha-color-fill-primary-normal-active); background-color: var(--ha-color-fill-primary-normal-active);
--md-list-item-label-text-color: var(--ha-color-on-primary-normal); --md-list-item-label-text-color: var(--primary-color);
--icon-primary-color: var(--ha-color-on-primary-normal); --icon-primary-color: var(--primary-color);
} }
.groups .selected ha-svg-icon { .groups .selected ha-svg-icon {
color: var(--ha-color-on-primary-normal); color: var(--primary-color);
} }
.collection-title { .collection-title {

View File

@@ -20,8 +20,7 @@ import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item"; import "../../../../components/ha-md-menu-item";
import { ACTION_BUILDING_BLOCKS } from "../../../../data/action"; import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
import type { ActionSidebarConfig } from "../../../../data/automation"; import type { ActionSidebarConfig } from "../../../../data/automation";
import { domainToName } from "../../../../data/integration"; import type { RepeatAction } from "../../../../data/script";
import type { RepeatAction, ServiceAction } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac"; import { isMac } from "../../../../util/is_mac";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor"; import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
@@ -84,23 +83,11 @@ export default class HaAutomationSidebarAction extends LitElement {
"ui.panel.config.automation.editor.actions.action" "ui.panel.config.automation.editor.actions.action"
); );
let title = const title =
this.hass.localize( this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${type}.label` as LocalizeKeys `ui.panel.config.automation.editor.actions.type.${type}.label` as LocalizeKeys
) || type; ) || type;
if (type === "service" && (actionConfig as ServiceAction).action) {
const [domain, service] = (actionConfig as ServiceAction).action!.split(
".",
2
);
title = `${domainToName(this.hass.localize, domain)}: ${
this.hass.localize(`component.${domain}.services.${service}.name`) ||
this.hass.services[domain][service]?.name ||
title
}`;
}
const description = isBuildingBlock const description = isBuildingBlock
? this.hass.localize( ? this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${type}.description.picker` as LocalizeKeys `ui.panel.config.automation.editor.actions.type.${type}.description.picker` as LocalizeKeys

View File

@@ -3,7 +3,6 @@ import {
mdiContentCopy, mdiContentCopy,
mdiContentCut, mdiContentCut,
mdiDelete, mdiDelete,
mdiIdentifier,
mdiPlayCircleOutline, mdiPlayCircleOutline,
mdiPlaylistEdit, mdiPlaylistEdit,
mdiPlusCircleMultipleOutline, mdiPlusCircleMultipleOutline,
@@ -41,8 +40,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
@property({ type: Number, attribute: "sidebar-key" }) @property({ type: Number, attribute: "sidebar-key" })
public sidebarKey?: number; public sidebarKey?: number;
@state() private _requestShowId = false;
@state() private _warnings?: string[]; @state() private _warnings?: string[];
@query(".sidebar-editor") @query(".sidebar-editor")
@@ -50,7 +47,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
protected willUpdate(changedProperties) { protected willUpdate(changedProperties) {
if (changedProperties.has("config")) { if (changedProperties.has("config")) {
this._requestShowId = false;
this._warnings = undefined; this._warnings = undefined;
if (this.config) { if (this.config) {
this.yamlMode = this.config.yamlMode; this.yamlMode = this.config.yamlMode;
@@ -105,24 +101,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
</div> </div>
</ha-md-menu-item> </ha-md-menu-item>
${!this.yamlMode &&
!("id" in this.config.config) &&
!this._requestShowId
? html`<ha-md-menu-item
slot="menu-items"
.clickAction=${this._showTriggerId}
.disabled=${this.disabled || type === "list"}
>
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>`
: nothing}
<ha-md-divider <ha-md-divider
slot="menu-items" slot="menu-items"
role="separator" role="separator"
@@ -272,7 +250,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
@value-changed=${this._valueChangedSidebar} @value-changed=${this._valueChangedSidebar}
@yaml-changed=${this._yamlChangedSidebar} @yaml-changed=${this._yamlChangedSidebar}
.uiSupported=${this.config.uiSupported} .uiSupported=${this.config.uiSupported}
.showId=${this._requestShowId}
.yamlMode=${this.yamlMode} .yamlMode=${this.yamlMode}
.disabled=${this.disabled} .disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable} @ui-mode-not-available=${this._handleUiModeNotAvailable}
@@ -315,10 +292,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
fireEvent(this, "toggle-yaml-mode"); fireEvent(this, "toggle-yaml-mode");
}; };
private _showTriggerId = () => {
this._requestShowId = true;
};
static styles = [sidebarEditorStyles, overflowStyles]; static styles = [sidebarEditorStyles, overflowStyles];
} }

View File

@@ -29,8 +29,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false; @property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor; @query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
protected render() { protected render() {
@@ -38,8 +36,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
const yamlMode = this.yamlMode || !this.uiSupported; const yamlMode = this.yamlMode || !this.uiSupported;
const showId = "id" in this.trigger || this.showId;
return html` return html`
<div <div
class=${classMap({ class=${classMap({
@@ -74,15 +70,20 @@ export default class HaAutomationTriggerEditor extends LitElement {
></ha-yaml-editor> ></ha-yaml-editor>
` `
: html` : html`
${showId && !isTriggerList(this.trigger) ${!isTriggerList(this.trigger)
? html` ? html`
<ha-textfield <ha-textfield
.label=${this.hass.localize( .label=${`${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id" "ui.panel.config.automation.editor.triggers.id"
)} )} (${this.hass.localize(
"ui.panel.config.automation.editor.triggers.optional"
)})`}
.value=${this.trigger.id || ""} .value=${this.trigger.id || ""}
.disabled=${this.disabled} .disabled=${this.disabled}
@change=${this._idChanged} @change=${this._idChanged}
.helper=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id_helper"
)}
></ha-textfield> ></ha-textfield>
` `
: nothing} : nothing}

View File

@@ -19,7 +19,6 @@ import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template"; import { hasTemplate } from "../../../../../common/string/has-template";
import type { StateTrigger } from "../../../../../data/automation"; import type { StateTrigger } from "../../../../../data/automation";
import { ANY_STATE_VALUE } from "../../../../../components/entity/const";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import { baseTriggerStruct, forDictStruct } from "../../structs"; import { baseTriggerStruct, forDictStruct } from "../../structs";
import type { TriggerElement } from "../ha-automation-trigger-row"; import type { TriggerElement } from "../ha-automation-trigger-row";
@@ -37,12 +36,14 @@ const stateTriggerStruct = assign(
trigger: literal("state"), trigger: literal("state"),
entity_id: optional(union([string(), array(string())])), entity_id: optional(union([string(), array(string())])),
attribute: optional(string()), attribute: optional(string()),
from: optional(union([nullable(string()), array(string())])), from: optional(nullable(string())),
to: optional(union([nullable(string()), array(string())])), to: optional(nullable(string())),
for: optional(union([number(), string(), forDictStruct])), for: optional(union([number(), string(), forDictStruct])),
}) })
); );
const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";
@customElement("ha-automation-trigger-state") @customElement("ha-automation-trigger-state")
export class HaStateTrigger extends LitElement implements TriggerElement { export class HaStateTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -56,12 +57,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
} }
private _schema = memoizeOne( private _schema = memoizeOne(
( (localize: LocalizeFunc, attribute) =>
localize: LocalizeFunc,
attribute: string | undefined,
hideInFrom: string[],
hideInTo: string[]
) =>
[ [
{ {
name: "entity_id", name: "entity_id",
@@ -135,7 +131,6 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
}, },
selector: { selector: {
state: { state: {
multiple: true,
extra_options: (attribute extra_options: (attribute
? [] ? []
: [ : [
@@ -147,7 +142,6 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
}, },
]) as any, ]) as any,
attribute: attribute, attribute: attribute,
hide_states: hideInFrom,
}, },
}, },
}, },
@@ -158,7 +152,6 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
}, },
selector: { selector: {
state: { state: {
multiple: true,
extra_options: (attribute extra_options: (attribute
? [] ? []
: [ : [
@@ -170,7 +163,6 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
}, },
]) as any, ]) as any,
attribute: attribute, attribute: attribute,
hide_states: hideInTo,
}, },
}, },
}, },
@@ -215,15 +207,13 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
entity_id: ensureArray(this.trigger.entity_id), entity_id: ensureArray(this.trigger.entity_id),
for: trgFor, for: trgFor,
}; };
if (!data.attribute && data.to === null) {
data.to = this._normalizeStates(this.trigger.to, data.attribute); data.to = ANY_STATE_VALUE;
data.from = this._normalizeStates(this.trigger.from, data.attribute); }
const schema = this._schema( if (!data.attribute && data.from === null) {
this.hass.localize, data.from = ANY_STATE_VALUE;
this.trigger.attribute, }
data.to, const schema = this._schema(this.hass.localize, this.trigger.attribute);
data.from
);
return html` return html`
<ha-form <ha-form
@@ -241,60 +231,22 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
ev.stopPropagation(); ev.stopPropagation();
const newTrigger = ev.detail.value; const newTrigger = ev.detail.value;
newTrigger.to = this._applyAnyStateExclusive( if (newTrigger.to === ANY_STATE_VALUE) {
newTrigger.to, newTrigger.to = newTrigger.attribute ? undefined : null;
newTrigger.attribute
);
if (Array.isArray(newTrigger.to) && newTrigger.to.length === 0) {
delete newTrigger.to;
} }
newTrigger.from = this._applyAnyStateExclusive( if (newTrigger.from === ANY_STATE_VALUE) {
newTrigger.from, newTrigger.from = newTrigger.attribute ? undefined : null;
newTrigger.attribute
);
if (Array.isArray(newTrigger.from) && newTrigger.from.length === 0) {
delete newTrigger.from;
} }
Object.keys(newTrigger).forEach((key) => { Object.keys(newTrigger).forEach((key) =>
const val = newTrigger[key]; newTrigger[key] === undefined || newTrigger[key] === ""
if (val === undefined || val === "") { ? delete newTrigger[key]
delete newTrigger[key]; : {}
} );
});
fireEvent(this, "value-changed", { value: newTrigger }); fireEvent(this, "value-changed", { value: newTrigger });
} }
private _applyAnyStateExclusive(
val: string | string[] | null | undefined,
attribute?: string
): string | string[] | null | undefined {
const anyStateSelected = Array.isArray(val)
? val.includes(ANY_STATE_VALUE)
: val === ANY_STATE_VALUE;
if (anyStateSelected) {
// Any state is exclusive: null if no attribute, undefined if attribute
return attribute ? undefined : null;
}
return val;
}
private _normalizeStates(
value: string | string[] | null | undefined,
attribute?: string
): string[] {
// If no attribute is selected and backend value is null,
// expose it as the special ANY state option in the UI.
if (!attribute && value === null) {
return [ANY_STATE_VALUE];
}
if (value === undefined || value === null) {
return [];
}
return ensureArray(value);
}
private _computeLabelCallback = ( private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>> schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => ): string =>

View File

@@ -125,6 +125,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@query("#overflow-menu") private _overflowMenu?: HaMdMenu; @query("#overflow-menu") private _overflowMenu?: HaMdMenu;
private _overflowBackup?: BackupContent;
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
window.addEventListener("location-changed", this._locationChanged); window.addEventListener("location-changed", this._locationChanged);
@@ -260,7 +262,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
type: "overflow-menu", type: "overflow-menu",
template: (backup) => html` template: (backup) => html`
<ha-icon-button <ha-icon-button
.backup=${backup} .selected=${backup}
.label=${this.hass.localize("ui.common.overflow_menu")} .label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
@click=${this._toggleOverflowMenu} @click=${this._toggleOverflowMenu}
@@ -292,6 +294,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
this._overflowMenu.close(); this._overflowMenu.close();
return; return;
} }
this._overflowBackup = ev.target.selected;
this._overflowMenu.anchorElement = ev.target; this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show(); this._overflowMenu.show();
}; };
@@ -372,14 +375,16 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
clickable clickable
id="backup_id" id="backup_id"
has-filters has-filters
.filters=${Object.values(this._filters).filter((filter) => .filters=${
Object.values(this._filters).filter((filter) =>
Array.isArray(filter) Array.isArray(filter)
? filter.length ? filter.length
: filter && : filter &&
Object.values(filter).some((val) => Object.values(filter).some((val) =>
Array.isArray(val) ? val.length : val Array.isArray(val) ? val.length : val
) )
).length} ).length
}
selectable selectable
.selected=${this._selected.length} .selected=${this._selected.length}
.initialGroupColumn=${this._activeGrouping} .initialGroupColumn=${this._activeGrouping}
@@ -421,7 +426,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
</div> </div>
<div slot="selection-bar"> <div slot="selection-bar">
${!this.narrow ${
!this.narrow
? html` ? html`
<ha-button <ha-button
appearance="plain" appearance="plain"
@@ -442,7 +448,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
class="warning" class="warning"
@click=${this._deleteSelected} @click=${this._deleteSelected}
></ha-icon-button> ></ha-icon-button>
`} `
}
</div> </div>
<ha-filter-states <ha-filter-states
@@ -455,7 +462,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
expanded expanded
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-filter-states> ></ha-filter-states>
${!this._needsOnboarding ${
!this._needsOnboarding
? html` ? html`
<ha-fab <ha-fab
slot="fab" slot="fab"
@@ -476,7 +484,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
></ha-svg-icon>`} ></ha-svg-icon>`}
</ha-fab> </ha-fab>
` `
: nothing} : nothing
}
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
<ha-md-menu id="overflow-menu" positioning="fixed"> <ha-md-menu id="overflow-menu" positioning="fixed">
<ha-md-menu-item .clickAction=${this._downloadBackup}> <ha-md-menu-item .clickAction=${this._downloadBackup}>
@@ -488,6 +497,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.common.delete")} ${this.hass.localize("ui.common.delete")}
</ha-md-menu-item> </ha-md-menu-item>
</ha-md-menu> </ha-md-menu>
>
</ha-icon-overflow-menu>
`; `;
} }
@@ -561,17 +572,15 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
navigate(`/config/backup/details/${id}`); navigate(`/config/backup/details/${id}`);
} }
private _downloadBackup = async (ev): Promise<void> => { private async _downloadBackup(): Promise<void> {
const backup = ev.parentElement.anchorElement.backup; if (!this._overflowBackup) {
if (!backup) {
return; return;
} }
downloadBackup(this.hass, this, backup, this.config); downloadBackup(this.hass, this, this._overflowBackup, this.config);
}; }
private _deleteBackup = async (ev): Promise<void> => { private async _deleteBackup(): Promise<void> {
const backup = ev.parentElement.anchorElement.backup; if (!this._overflowBackup) {
if (!backup) {
return; return;
} }
@@ -587,9 +596,11 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
} }
try { try {
await deleteBackup(this.hass, backup.backup_id); await deleteBackup(this.hass, this._overflowBackup.backup_id);
if (this._selected.includes(backup.backup_id)) { if (this._selected.includes(this._overflowBackup.backup_id)) {
this._selected = this._selected.filter((id) => id !== backup.backup_id); this._selected = this._selected.filter(
(id) => id !== this._overflowBackup!.backup_id
);
} }
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
@@ -601,7 +612,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
return; return;
} }
fireEvent(this, "ha-refresh-backup-info"); fireEvent(this, "ha-refresh-backup-info");
}; }
private async _deleteSelected() { private async _deleteSelected() {
const confirm = await showConfirmationDialog(this, { const confirm = await showConfirmationDialog(this, {

View File

@@ -98,7 +98,7 @@ class DialogCategoryDetail extends LitElement {
</div> </div>
<ha-button <ha-button
appearance="plain" appearance="plain"
slot="secondaryAction" slot="primaryAction"
@click=${this.closeDialog} @click=${this.closeDialog}
> >
${this.hass.localize("ui.common.cancel")} ${this.hass.localize("ui.common.cancel")}

View File

@@ -20,7 +20,6 @@ import { subscribeEntityRegistry } from "../../../data/entity_registry";
import type { UpdateEntity } from "../../../data/update"; import type { UpdateEntity } from "../../../data/update";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../../../components/ha-progress-ring";
@customElement("ha-config-updates") @customElement("ha-config-updates")
class HaConfigUpdates extends SubscribeMixin(LitElement) { class HaConfigUpdates extends SubscribeMixin(LitElement) {
@@ -57,29 +56,6 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
this._entities?.find((entity) => entity.entity_id === entityId) this._entities?.find((entity) => entity.entity_id === entityId)
); );
private _renderUpdateProgress(entity: UpdateEntity) {
if (entity.attributes.update_percentage != null) {
return html`<ha-progress-ring
size="small"
.value=${entity.attributes.update_percentage}
.label=${this.hass.localize(
"ui.panel.config.updates.update_in_progress"
)}
></ha-progress-ring>`;
}
if (entity.attributes.in_progress) {
return html`<ha-spinner
size="small"
.ariaLabel=${this.hass.localize(
"ui.panel.config.updates.update_in_progress"
)}
></ha-spinner>`;
}
return html`<ha-icon-next></ha-icon-next>`;
}
protected render() { protected render() {
if (!this.updateEntities?.length) { if (!this.updateEntities?.length) {
return nothing; return nothing;
@@ -130,9 +106,13 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
)} )}
></state-badge> ></state-badge>
${this.narrow && entity.attributes.in_progress ${this.narrow && entity.attributes.in_progress
? html`<div class="absolute"> ? html`<ha-spinner
${this._renderUpdateProgress(entity)} class="absolute"
</div>` size="small"
.ariaLabel=${this.hass.localize(
"ui.panel.config.updates.update_in_progress"
)}
></ha-spinner>`
: nothing} : nothing}
</div> </div>
<span slot="headline" <span slot="headline"
@@ -148,9 +128,16 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
: nothing} : nothing}
</span> </span>
${!this.narrow ${!this.narrow
? entity.attributes.in_progress
? html`<div slot="end"> ? html`<div slot="end">
${this._renderUpdateProgress(entity)} <ha-spinner
size="small"
.ariaLabel=${this.hass.localize(
"ui.panel.config.updates.update_in_progress"
)}
></ha-spinner>
</div>` </div>`
: html`<ha-icon-next slot="end"></ha-icon-next>`
: nothing} : nothing}
</ha-md-list-item> </ha-md-list-item>
`; `;
@@ -206,13 +193,13 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
div[slot="start"] { div[slot="start"] {
position: relative; position: relative;
} }
div.absolute { ha-spinner.absolute {
position: absolute; position: absolute;
left: 6px; left: 6px;
top: 6px; top: 6px;
} }
state-badge.updating { state-badge.updating {
opacity: 0.2; opacity: 0.5;
} }
`, `,
]; ];

View File

@@ -228,7 +228,7 @@ export class HaDeviceEntitiesCard extends LitElement {
addEntitiesToLovelaceView( addEntitiesToLovelaceView(
this, this,
this.hass, this.hass,
computeCards(this.hass, entities, { computeCards(this.hass.states, entities, {
title: this.deviceName, title: this.deviceName,
}), }),
computeSection(entities, { computeSection(entities, {

View File

@@ -9,7 +9,6 @@ import "../../../../components/ha-dialog";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-formfield"; import "../../../../components/ha-formfield";
import "../../../../components/ha-radio"; import "../../../../components/ha-radio";
import "../../../../components/ha-markdown";
import type { HaRadio } from "../../../../components/ha-radio"; import type { HaRadio } from "../../../../components/ha-radio";
import type { import type {
FlowFromGridSourceEnergyPreference, FlowFromGridSourceEnergyPreference,
@@ -20,7 +19,11 @@ import {
emptyFlowToGridSourceEnergyPreference, emptyFlowToGridSourceEnergyPreference,
energyStatisticHelpUrl, energyStatisticHelpUrl,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { isExternalStatistic } from "../../../../data/recorder"; import {
getDisplayUnit,
getStatisticMetadata,
isExternalStatistic,
} from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor"; import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles"; import { haStyleDialog } from "../../../../resources/styles";
@@ -44,6 +47,8 @@ export class DialogEnergyGridFlowSettings
@state() private _costs?: "no-costs" | "number" | "entity" | "statistic"; @state() private _costs?: "no-costs" | "number" | "entity" | "statistic";
@state() private _pickedDisplayUnit?: string | null;
@state() private _energy_units?: string[]; @state() private _energy_units?: string[];
@state() private _error?: string; @state() private _error?: string;
@@ -76,6 +81,11 @@ export class DialogEnergyGridFlowSettings
: "stat_energy_to" : "stat_energy_to"
]; ];
this._pickedDisplayUnit = getDisplayUnit(
this.hass,
initialSourceId,
params.metadata
);
this._energy_units = ( this._energy_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "energy") await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
).units; ).units;
@@ -93,6 +103,7 @@ export class DialogEnergyGridFlowSettings
public closeDialog() { public closeDialog() {
this._params = undefined; this._params = undefined;
this._source = undefined; this._source = undefined;
this._pickedDisplayUnit = undefined;
this._error = undefined; this._error = undefined;
this._excludeList = undefined; this._excludeList = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -106,6 +117,10 @@ export class DialogEnergyGridFlowSettings
const pickableUnit = this._energy_units?.join(", ") || ""; const pickableUnit = this._energy_units?.join(", ") || "";
const unitPriceSensor = this._pickedDisplayUnit
? `${this.hass.config.currency}/${this._pickedDisplayUnit}`
: undefined;
const unitPriceFixed = `${this.hass.config.currency}/kWh`; const unitPriceFixed = `${this.hass.config.currency}/kWh`;
const externalSource = const externalSource =
@@ -231,15 +246,9 @@ export class DialogEnergyGridFlowSettings
.hass=${this.hass} .hass=${this.hass}
include-domains='["sensor", "input_number"]' include-domains='["sensor", "input_number"]'
.value=${this._source.entity_energy_price} .value=${this._source.entity_energy_price}
.label=${this.hass.localize( .label=${`${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_entity_input` `ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_entity_input`
)} )} ${unitPriceSensor ? ` (${unitPriceSensor})` : ""}`}
.helper=${html`<ha-markdown
.content=${this.hass.localize(
"ui.panel.config.energy.grid.flow_dialog.cost_entity_helper",
{ currency: this.hass.config.currency }
)}
></ha-markdown>`}
@value-changed=${this._priceEntityChanged} @value-changed=${this._priceEntityChanged}
></ha-entity-picker>` ></ha-entity-picker>`
: ""} : ""}
@@ -332,6 +341,16 @@ export class DialogEnergyGridFlowSettings
} }
private async _statisticChanged(ev: CustomEvent<{ value: string }>) { private async _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (ev.detail.value) {
const metadata = await getStatisticMetadata(this.hass, [ev.detail.value]);
this._pickedDisplayUnit = getDisplayUnit(
this.hass,
ev.detail.value,
metadata[0]
);
} else {
this._pickedDisplayUnit = undefined;
}
this._source = { this._source = {
...this._source!, ...this._source!,
[this._params!.direction === "from" [this._params!.direction === "from"

View File

@@ -4,7 +4,6 @@ import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-button";
import type { ExtEntityRegistryEntry } from "../../../../../data/entity_registry"; import type { ExtEntityRegistryEntry } from "../../../../../data/entity_registry";
import { removeEntityRegistryEntry } from "../../../../../data/entity_registry"; import { removeEntityRegistryEntry } from "../../../../../data/entity_registry";
import { HELPERS_CRUD } from "../../../../../data/helpers_crud"; import { HELPERS_CRUD } from "../../../../../data/helpers_crud";
@@ -23,6 +22,7 @@ import "../../../helpers/forms/ha-schedule-form";
import "../../../helpers/forms/ha-timer-form"; import "../../../helpers/forms/ha-timer-form";
import "../../../voice-assistants/entity-voice-settings"; import "../../../voice-assistants/entity-voice-settings";
import "../../entity-registry-settings-editor"; import "../../entity-registry-settings-editor";
import "../../../../../components/ha-button";
import type { EntityRegistrySettingsEditor } from "../../entity-registry-settings-editor"; import type { EntityRegistrySettingsEditor } from "../../entity-registry-settings-editor";
@customElement("entity-settings-helper-tab") @customElement("entity-settings-helper-tab")
@@ -72,25 +72,19 @@ export class EntitySettingsHelperTab extends LitElement {
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
${this._item === null
? html`<ha-alert alert-type="info"
>${this.hass.localize(
"ui.dialogs.helper_settings.yaml_not_editable"
)}</ha-alert
>`
: nothing}
${!this._componentLoaded ${!this._componentLoaded
? this.hass.localize( ? this.hass.localize(
"ui.dialogs.helper_settings.platform_not_loaded", "ui.dialogs.helper_settings.platform_not_loaded",
{ platform: this.entry.platform } { platform: this.entry.platform }
) )
: this._item === null
? this.hass.localize("ui.dialogs.helper_settings.yaml_not_editable")
: html` : html`
<span @value-changed=${this._valueChanged}> <span @value-changed=${this._valueChanged}>
${dynamicElement(`ha-${this.entry.platform}-form`, { ${dynamicElement(`ha-${this.entry.platform}-form`, {
hass: this.hass, hass: this.hass,
item: this._item, item: this._item,
entry: this.entry, entry: this.entry,
disabled: this._item === null,
})} })}
</span> </span>
`} `}
@@ -128,9 +122,6 @@ export class EntitySettingsHelperTab extends LitElement {
} }
private _valueChanged(ev: CustomEvent): void { private _valueChanged(ev: CustomEvent): void {
if (this._item === null) {
return;
}
this._error = undefined; this._error = undefined;
this._item = ev.detail.value; this._item = ev.detail.value;
} }
@@ -204,10 +195,6 @@ export class EntitySettingsHelperTab extends LitElement {
display: block; display: block;
padding: 0 !important; padding: 0 !important;
} }
ha-alert {
display: block;
margin-bottom: var(--ha-space-4);
}
.form { .form {
padding: 20px 24px; padding: 20px 24px;
} }

View File

@@ -784,7 +784,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
<ha-labels-picker <ha-labels-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this._labels} .value=${this._labels}
.disabled=${!!this.disabled} .disabled=${this.disabled}
@value-changed=${this._labelsChanged} @value-changed=${this._labelsChanged}
></ha-labels-picker> ></ha-labels-picker>
${this._cameraPrefs ${this._cameraPrefs

View File

@@ -153,8 +153,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
}, },
yAxis: { yAxis: {
type: "value", type: "value",
min: 0,
max: 100,
splitLine: { splitLine: {
show: true, show: true,
}, },

View File

@@ -17,8 +17,6 @@ class HaCounterForm extends LitElement {
@property({ type: Boolean }) public new = false; @property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Partial<Counter>; private _item?: Partial<Counter>;
@state() private _name!: string; @state() private _name!: string;
@@ -84,7 +82,6 @@ class HaCounterForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg" "ui.dialogs.helper_settings.required_error_msg"
)} )}
dialogInitialFocus dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .hass=${this.hass}
@@ -94,7 +91,6 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon" "ui.dialogs.helper_settings.generic.icon"
)} )}
.disabled=${this.disabled}
></ha-icon-picker> ></ha-icon-picker>
<ha-textfield <ha-textfield
.value=${this._minimum} .value=${this._minimum}
@@ -104,7 +100,6 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.minimum" "ui.dialogs.helper_settings.counter.minimum"
)} )}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-textfield <ha-textfield
.value=${this._maximum} .value=${this._maximum}
@@ -114,7 +109,6 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.maximum" "ui.dialogs.helper_settings.counter.maximum"
)} )}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-textfield <ha-textfield
.value=${this._initial} .value=${this._initial}
@@ -124,7 +118,6 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.initial" "ui.dialogs.helper_settings.counter.initial"
)} )}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-expansion-panel <ha-expansion-panel
header=${this.hass.localize( header=${this.hass.localize(
@@ -140,14 +133,12 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.step" "ui.dialogs.helper_settings.counter.step"
)} )}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<div class="row"> <div class="row">
<ha-switch <ha-switch
.checked=${this._restore} .checked=${this._restore}
.configValue=${"restore"} .configValue=${"restore"}
@change=${this._valueChanged} @change=${this._valueChanged}
.disabled=${this.disabled}
> >
</ha-switch> </ha-switch>
<div> <div>

View File

@@ -14,8 +14,6 @@ class HaInputBooleanForm extends LitElement {
@property({ type: Boolean }) public new = false; @property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputBoolean; private _item?: InputBoolean;
@state() private _name!: string; @state() private _name!: string;
@@ -61,7 +59,6 @@ class HaInputBooleanForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg" "ui.dialogs.helper_settings.required_error_msg"
)} )}
dialogInitialFocus dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .hass=${this.hass}
@@ -71,7 +68,6 @@ class HaInputBooleanForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon" "ui.dialogs.helper_settings.generic.icon"
)} )}
.disabled=${this.disabled}
></ha-icon-picker> ></ha-icon-picker>
</div> </div>
`; `;

View File

@@ -14,8 +14,6 @@ class HaInputButtonForm extends LitElement {
@property({ type: Boolean }) public new = false; @property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
@state() private _name!: string; @state() private _name!: string;
@state() private _icon!: string; @state() private _icon!: string;
@@ -61,7 +59,6 @@ class HaInputButtonForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg" "ui.dialogs.helper_settings.required_error_msg"
)} )}
dialogInitialFocus dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .hass=${this.hass}
@@ -71,7 +68,6 @@ class HaInputButtonForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon" "ui.dialogs.helper_settings.generic.icon"
)} )}
.disabled=${this.disabled}
></ha-icon-picker> ></ha-icon-picker>
</div> </div>
`; `;

View File

@@ -17,8 +17,6 @@ class HaInputDateTimeForm extends LitElement {
@property({ type: Boolean }) public new = false; @property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputDateTime; private _item?: InputDateTime;
@state() private _name!: string; @state() private _name!: string;
@@ -75,7 +73,6 @@ class HaInputDateTimeForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg" "ui.dialogs.helper_settings.required_error_msg"
)} )}
dialogInitialFocus dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .hass=${this.hass}
@@ -85,7 +82,6 @@ class HaInputDateTimeForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon" "ui.dialogs.helper_settings.generic.icon"
)} )}
.disabled=${this.disabled}
></ha-icon-picker> ></ha-icon-picker>
<br /> <br />
${this.hass.localize("ui.dialogs.helper_settings.input_datetime.mode")}: ${this.hass.localize("ui.dialogs.helper_settings.input_datetime.mode")}:
@@ -101,7 +97,6 @@ class HaInputDateTimeForm extends LitElement {
value="date" value="date"
.checked=${this._mode === "date"} .checked=${this._mode === "date"}
@change=${this._modeChanged} @change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio> ></ha-radio>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -114,7 +109,6 @@ class HaInputDateTimeForm extends LitElement {
value="time" value="time"
.checked=${this._mode === "time"} .checked=${this._mode === "time"}
@change=${this._modeChanged} @change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio> ></ha-radio>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -127,7 +121,6 @@ class HaInputDateTimeForm extends LitElement {
value="datetime" value="datetime"
.checked=${this._mode === "datetime"} .checked=${this._mode === "datetime"}
@change=${this._modeChanged} @change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio> ></ha-radio>
</ha-formfield> </ha-formfield>
</div> </div>

View File

@@ -18,8 +18,6 @@ class HaInputNumberForm extends LitElement {
@property({ type: Boolean }) public new = false; @property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Partial<InputNumber>; private _item?: Partial<InputNumber>;
@state() private _name!: string; @state() private _name!: string;
@@ -91,7 +89,6 @@ class HaInputNumberForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg" "ui.dialogs.helper_settings.required_error_msg"
)} )}
dialogInitialFocus dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .hass=${this.hass}
@@ -101,7 +98,6 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon" "ui.dialogs.helper_settings.generic.icon"
)} )}
.disabled=${this.disabled}
></ha-icon-picker> ></ha-icon-picker>
<ha-textfield <ha-textfield
.value=${this._min} .value=${this._min}
@@ -112,7 +108,6 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.min" "ui.dialogs.helper_settings.input_number.min"
)} )}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-textfield <ha-textfield
.value=${this._max} .value=${this._max}
@@ -123,7 +118,6 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.max" "ui.dialogs.helper_settings.input_number.max"
)} )}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-expansion-panel <ha-expansion-panel
header=${this.hass.localize( header=${this.hass.localize(
@@ -145,7 +139,6 @@ class HaInputNumberForm extends LitElement {
value="slider" value="slider"
.checked=${this._mode === "slider"} .checked=${this._mode === "slider"}
@change=${this._modeChanged} @change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio> ></ha-radio>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -158,7 +151,6 @@ class HaInputNumberForm extends LitElement {
value="box" value="box"
.checked=${this._mode === "box"} .checked=${this._mode === "box"}
@change=${this._modeChanged} @change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio> ></ha-radio>
</ha-formfield> </ha-formfield>
</div> </div>
@@ -171,7 +163,6 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.step" "ui.dialogs.helper_settings.input_number.step"
)} )}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-textfield <ha-textfield
@@ -181,7 +172,6 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.unit_of_measurement" "ui.dialogs.helper_settings.input_number.unit_of_measurement"
)} )}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
</ha-expansion-panel> </ha-expansion-panel>
</div> </div>

View File

@@ -23,8 +23,6 @@ class HaInputSelectForm extends LitElement {
@property({ type: Boolean }) public new = false; @property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputSelect; private _item?: InputSelect;
@state() private _name!: string; @state() private _name!: string;
@@ -88,7 +86,6 @@ class HaInputSelectForm extends LitElement {
)} )}
.configValue=${"name"} .configValue=${"name"}
@input=${this._valueChanged} @input=${this._valueChanged}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .hass=${this.hass}
@@ -98,18 +95,13 @@ class HaInputSelectForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon" "ui.dialogs.helper_settings.generic.icon"
)} )}
.disabled=${this.disabled}
></ha-icon-picker> ></ha-icon-picker>
<div class="header"> <div class="header">
${this.hass!.localize( ${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.options" "ui.dialogs.helper_settings.input_select.options"
)}: )}:
</div> </div>
<ha-sortable <ha-sortable @item-moved=${this._optionMoved} handle-selector=".handle">
@item-moved=${this._optionMoved}
handle-selector=".handle"
.disabled=${this.disabled}
>
<ha-list class="options"> <ha-list class="options">
${this._options.length ${this._options.length
? repeat( ? repeat(
@@ -132,7 +124,6 @@ class HaInputSelectForm extends LitElement {
"ui.dialogs.helper_settings.input_select.remove_option" "ui.dialogs.helper_settings.input_select.remove_option"
)} )}
@click=${this._removeOption} @click=${this._removeOption}
.disabled=${this.disabled}
.path=${mdiDelete} .path=${mdiDelete}
></ha-icon-button> ></ha-icon-button>
</ha-list-item> </ha-list-item>
@@ -155,13 +146,8 @@ class HaInputSelectForm extends LitElement {
"ui.dialogs.helper_settings.input_select.add_option" "ui.dialogs.helper_settings.input_select.add_option"
)} )}
@keydown=${this._handleKeyAdd} @keydown=${this._handleKeyAdd}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-button <ha-button size="small" appearance="plain" @click=${this._addOption}
size="small"
appearance="plain"
@click=${this._addOption}
.disabled=${this.disabled}
>${this.hass!.localize( >${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.add" "ui.dialogs.helper_settings.input_select.add"
)}</ha-button )}</ha-button

View File

@@ -19,8 +19,6 @@ class HaInputTextForm extends LitElement {
@property({ type: Boolean }) public new = false; @property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputText; private _item?: InputText;
@state() private _name!: string; @state() private _name!: string;
@@ -81,7 +79,6 @@ class HaInputTextForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg" "ui.dialogs.helper_settings.required_error_msg"
)} )}
dialogInitialFocus dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .hass=${this.hass}
@@ -91,7 +88,6 @@ class HaInputTextForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon" "ui.dialogs.helper_settings.generic.icon"
)} )}
.disabled=${this.disabled}
></ha-icon-picker> ></ha-icon-picker>
<ha-expansion-panel <ha-expansion-panel
header=${this.hass.localize( header=${this.hass.localize(
@@ -109,7 +105,6 @@ class HaInputTextForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.min" "ui.dialogs.helper_settings.input_text.min"
)} )}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-textfield <ha-textfield
.value=${this._max} .value=${this._max}
@@ -134,7 +129,6 @@ class HaInputTextForm extends LitElement {
value="text" value="text"
.checked=${this._mode === "text"} .checked=${this._mode === "text"}
@change=${this._modeChanged} @change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio> ></ha-radio>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -147,7 +141,6 @@ class HaInputTextForm extends LitElement {
value="password" value="password"
.checked=${this._mode === "password"} .checked=${this._mode === "password"}
@change=${this._modeChanged} @change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio> ></ha-radio>
</ha-formfield> </ha-formfield>
</div> </div>
@@ -161,7 +154,6 @@ class HaInputTextForm extends LitElement {
.helper=${this.hass!.localize( .helper=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.pattern_helper" "ui.dialogs.helper_settings.input_text.pattern_helper"
)} )}
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
</ha-expansion-panel> </ha-expansion-panel>
</div> </div>

View File

@@ -17,9 +17,9 @@ import "../../../../components/ha-textfield";
import type { Schedule, ScheduleDay } from "../../../../data/schedule"; import type { Schedule, ScheduleDay } from "../../../../data/schedule";
import { weekdays } from "../../../../data/schedule"; import { weekdays } from "../../../../data/schedule";
import { TimeZone } from "../../../../data/translation"; import { TimeZone } from "../../../../data/translation";
import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info";
const defaultFullCalendarConfig: CalendarOptions = { const defaultFullCalendarConfig: CalendarOptions = {
plugins: [timeGridPlugin, interactionPlugin], plugins: [timeGridPlugin, interactionPlugin],
@@ -43,8 +43,6 @@ class HaScheduleForm extends LitElement {
@property({ type: Boolean }) public new = false; @property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
@state() private _name!: string; @state() private _name!: string;
@state() private _icon!: string; @state() private _icon!: string;
@@ -134,7 +132,6 @@ class HaScheduleForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg" "ui.dialogs.helper_settings.required_error_msg"
)} )}
dialogInitialFocus dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .hass=${this.hass}
@@ -144,9 +141,8 @@ class HaScheduleForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon" "ui.dialogs.helper_settings.generic.icon"
)} )}
.disabled=${this.disabled}
></ha-icon-picker> ></ha-icon-picker>
${!this.disabled ? html`<div id="calendar"></div>` : nothing} <div id="calendar"></div>
</div> </div>
`; `;
} }
@@ -179,10 +175,8 @@ class HaScheduleForm extends LitElement {
} }
protected firstUpdated(): void { protected firstUpdated(): void {
if (!this.disabled) {
this._setupCalendar(); this._setupCalendar();
} }
}
private _setupCalendar(): void { private _setupCalendar(): void {
const config: CalendarOptions = { const config: CalendarOptions = {

View File

@@ -1,18 +1,18 @@
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { createDurationData } from "../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-checkbox"; import "../../../../components/ha-checkbox";
import "../../../../components/ha-duration-input";
import type { HaDurationData } from "../../../../components/ha-duration-input";
import "../../../../components/ha-formfield"; import "../../../../components/ha-formfield";
import "../../../../components/ha-icon-picker"; import "../../../../components/ha-icon-picker";
import "../../../../components/ha-duration-input";
import "../../../../components/ha-textfield"; import "../../../../components/ha-textfield";
import type { ForDict } from "../../../../data/automation";
import type { DurationDict, Timer } from "../../../../data/timer"; import type { DurationDict, Timer } from "../../../../data/timer";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { createDurationData } from "../../../../common/datetime/create_duration_data";
import type { HaDurationData } from "../../../../components/ha-duration-input";
import type { ForDict } from "../../../../data/automation";
@customElement("ha-timer-form") @customElement("ha-timer-form")
class HaTimerForm extends LitElement { class HaTimerForm extends LitElement {
@@ -20,8 +20,6 @@ class HaTimerForm extends LitElement {
@property({ type: Boolean }) public new = false; @property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Timer; private _item?: Timer;
@state() private _name!: string; @state() private _name!: string;
@@ -79,7 +77,6 @@ class HaTimerForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg" "ui.dialogs.helper_settings.required_error_msg"
)} )}
dialogInitialFocus dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield> ></ha-textfield>
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .hass=${this.hass}
@@ -89,13 +86,11 @@ class HaTimerForm extends LitElement {
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon" "ui.dialogs.helper_settings.generic.icon"
)} )}
.disabled=${this.disabled}
></ha-icon-picker> ></ha-icon-picker>
<ha-duration-input <ha-duration-input
.configValue=${"duration"} .configValue=${"duration"}
.data=${this._duration_data} .data=${this._duration_data}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.disabled=${this.disabled}
></ha-duration-input> ></ha-duration-input>
<ha-formfield <ha-formfield
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -106,7 +101,6 @@ class HaTimerForm extends LitElement {
.configValue=${"restore"} .configValue=${"restore"}
.checked=${this._restore} .checked=${this._restore}
@click=${this._toggleRestore} @click=${this._toggleRestore}
.disabled=${this.disabled}
> >
</ha-checkbox> </ha-checkbox>
</ha-formfield> </ha-formfield>
@@ -136,9 +130,6 @@ class HaTimerForm extends LitElement {
} }
private _toggleRestore() { private _toggleRestore() {
if (this.disabled) {
return;
}
this._restore = !this._restore; this._restore = !this._restore;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { ...this._item, restore: this._restore }, value: { ...this._item, restore: this._restore },

View File

@@ -107,8 +107,6 @@ class HaConfigInfo extends LitElement {
const customUiList: { name: string; url: string; version: string }[] = const customUiList: { name: string; url: string; version: string }[] =
(window as any).CUSTOM_UI_LIST || []; (window as any).CUSTOM_UI_LIST || [];
const isDark = this.hass.themes?.darkMode || false;
return html` return html`
<hass-subpage <hass-subpage
.hass=${this.hass} .hass=${this.hass}
@@ -188,7 +186,7 @@ class HaConfigInfo extends LitElement {
: nothing} : nothing}
</ul> </ul>
</ha-card> </ha-card>
<ha-card outlined class="ohf ${isDark ? "dark" : ""}"> <ha-card outlined class="ohf">
<div> <div>
${this.hass.localize("ui.panel.config.info.proud_part_of")} ${this.hass.localize("ui.panel.config.info.proud_part_of")}
</div> </div>
@@ -348,10 +346,6 @@ class HaConfigInfo extends LitElement {
max-width: 250px; max-width: 250px;
} }
.ohf.dark img {
color-scheme: dark;
}
.versions { .versions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -790,10 +790,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
); );
const timeString = new Date().toISOString().replace(/:/g, "-"); const timeString = new Date().toISOString().replace(/:/g, "-");
const logFileName = `home-assistant_${integration}_${timeString}.log`; const logFileName = `home-assistant_${integration}_${timeString}.log`;
const signedUrl = await getSignedPath( const signedUrl = await getSignedPath(this.hass, getErrorLogDownloadUrl);
this.hass,
getErrorLogDownloadUrl(this.hass)
);
fileDownload(signedUrl.path, logFileName); fileDownload(signedUrl.path, logFileName);
} }

View File

@@ -8,7 +8,6 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { relativeTime } from "../../../../../common/datetime/relative_time"; import { relativeTime } from "../../../../../common/datetime/relative_time";
import { getDeviceContext } from "../../../../../common/entity/context/get_device_context";
import { navigate } from "../../../../../common/navigate"; import { navigate } from "../../../../../common/navigate";
import { throttle } from "../../../../../common/util/throttle"; import { throttle } from "../../../../../common/util/throttle";
import "../../../../../components/chart/ha-network-graph"; import "../../../../../components/chart/ha-network-graph";
@@ -195,17 +194,11 @@ export class BluetoothNetworkVisualization extends LitElement {
]; ];
const links: NetworkLink[] = []; const links: NetworkLink[] = [];
Object.values(scanners).forEach((scanner) => { Object.values(scanners).forEach((scanner) => {
const scannerDevice = this._sourceDevices[scanner.source] as const scannerDevice = this._sourceDevices[scanner.source];
| DeviceRegistryEntry
| undefined;
const area = scannerDevice
? getDeviceContext(scannerDevice, this.hass).area
: undefined;
nodes.push({ nodes.push({
id: scanner.source, id: scanner.source,
name: name:
scannerDevice?.name_by_user || scannerDevice?.name || scanner.name, scannerDevice?.name_by_user || scannerDevice?.name || scanner.name,
context: area?.name,
category: 1, category: 1,
value: 5, value: 5,
symbol: "circle", symbol: "circle",
@@ -238,16 +231,10 @@ export class BluetoothNetworkVisualization extends LitElement {
}); });
return; return;
} }
const device = this._sourceDevices[node.address] as const device = this._sourceDevices[node.address];
| DeviceRegistryEntry
| undefined;
const area = device
? getDeviceContext(device, this.hass).area
: undefined;
nodes.push({ nodes.push({
id: node.address, id: node.address,
name: this._getBluetoothDeviceName(node.address), name: this._getBluetoothDeviceName(node.address),
context: area?.name,
value: device ? 1 : 0, value: device ? 1 : 0,
category: device ? 2 : 3, category: device ? 2 : 3,
symbolSize: 20, symbolSize: 20,
@@ -307,24 +294,19 @@ export class BluetoothNetworkVisualization extends LitElement {
const btDevice = this._data.find((d) => d.address === address); const btDevice = this._data.find((d) => d.address === address);
if (btDevice) { if (btDevice) {
tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}<br><b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b> ${btDevice.rssi}<br><b>${this.hass.localize("ui.panel.config.bluetooth.source")}:</b> ${btDevice.source}<br><b>${this.hass.localize("ui.panel.config.bluetooth.updated")}:</b> ${relativeTime(new Date(btDevice.time * 1000), this.hass.locale)}`; tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}<br><b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b> ${btDevice.rssi}<br><b>${this.hass.localize("ui.panel.config.bluetooth.source")}:</b> ${btDevice.source}<br><b>${this.hass.localize("ui.panel.config.bluetooth.updated")}:</b> ${relativeTime(new Date(btDevice.time * 1000), this.hass.locale)}`;
const device = this._sourceDevices[address];
if (device) {
const area = getDeviceContext(device, this.hass).area;
if (area) {
tooltipText += `<br><b>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b>${area.name}`;
}
}
} else { } else {
const device = this._sourceDevices[address]; const device = this._sourceDevices[address];
if (device) { if (device) {
tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}`; tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}`;
const area = getDeviceContext(device, this.hass).area; if (device.area_id) {
const area = this.hass.areas[device.area_id];
if (area) { if (area) {
tooltipText += `<br><b>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b>${area.name}`; tooltipText += `<br><b>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b>${area.name}`;
} }
} }
} }
} }
}
return tooltipText; return tooltipText;
}; };

View File

@@ -19,8 +19,6 @@ import "../../../../../layouts/hass-tabs-subpage";
import type { HomeAssistant, Route } from "../../../../../types"; import type { HomeAssistant, Route } from "../../../../../types";
import { formatAsPaddedHex } from "./functions"; import { formatAsPaddedHex } from "./functions";
import { zhaTabs } from "./zha-config-dashboard"; import { zhaTabs } from "./zha-config-dashboard";
import type { DeviceRegistryEntry } from "../../../../../data/device_registry";
import { getDeviceContext } from "../../../../../common/entity/context/get_device_context";
@customElement("zha-network-visualization-page") @customElement("zha-network-visualization-page")
export class ZHANetworkVisualizationPage extends LitElement { export class ZHANetworkVisualizationPage extends LitElement {
@@ -119,11 +117,8 @@ export class ZHANetworkVisualizationPage extends LitElement {
} else { } else {
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.device_not_in_db")}</b>`; label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.device_not_in_db")}</b>`;
} }
const haDevice = this.hass.devices[device.device_reg_id] as if (device.area_id) {
| DeviceRegistryEntry const area = this.hass.areas[device.area_id];
| undefined;
if (haDevice) {
const area = getDeviceContext(haDevice, this.hass).area;
if (area) { if (area) {
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.area")}: </b>${area.name}`; label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.area")}: </b>${area.name}`;
} }
@@ -209,17 +204,10 @@ export class ZHANetworkVisualizationPage extends LitElement {
category = 2; // End Device category = 2; // End Device
} }
const haDevice = this.hass.devices[device.device_reg_id] as
| DeviceRegistryEntry
| undefined;
const area = haDevice
? getDeviceContext(haDevice, this.hass).area
: undefined;
// Create node // Create node
nodes.push({ nodes.push({
id: device.ieee, id: device.ieee,
name: device.user_given_name || device.name || device.ieee, name: device.user_given_name || device.name || device.ieee,
context: area?.name,
category, category,
value: isCoordinator ? 3 : device.device_type === "Router" ? 2 : 1, value: isCoordinator ? 3 : device.device_type === "Router" ? 2 : 1,
symbolSize: isCoordinator symbolSize: isCoordinator

View File

@@ -5,7 +5,6 @@ import type {
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { getDeviceContext } from "../../../../../common/entity/context/get_device_context";
import { navigate } from "../../../../../common/navigate"; import { navigate } from "../../../../../common/navigate";
import { debounce } from "../../../../../common/util/debounce"; import { debounce } from "../../../../../common/util/debounce";
import "../../../../../components/chart/ha-network-graph"; import "../../../../../components/chart/ha-network-graph";
@@ -125,7 +124,7 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
return tip; return tip;
} }
const { id, name } = data as any; const { id, name } = data as any;
const device = this._devices[id] as DeviceRegistryEntry | undefined; const device = this._devices[id];
const nodeStatus = this._nodeStatuses[id]; const nodeStatus = this._nodeStatuses[id];
let tip = `${(params as any).marker} ${name}`; let tip = `${(params as any).marker} ${name}`;
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.node_id")}:</b> ${id}`; tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.node_id")}:</b> ${id}`;
@@ -139,12 +138,6 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
tip += `<br><b>Z-Wave Plus:</b> ${this.hass.localize("ui.panel.config.zwave_js.visualization.version")} ${nodeStatus.zwave_plus_version}`; tip += `<br><b>Z-Wave Plus:</b> ${this.hass.localize("ui.panel.config.zwave_js.visualization.version")} ${nodeStatus.zwave_plus_version}`;
} }
} }
if (device) {
const area = getDeviceContext(device, this.hass).area;
if (area) {
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.area")}:</b> ${area.name}`;
}
}
return tip; return tip;
}; };
@@ -204,16 +197,10 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
if (node.is_controller_node) { if (node.is_controller_node) {
controllerNode = node.node_id; controllerNode = node.node_id;
} }
const device = this._devices[node.node_id] as const device = this._devices[node.node_id];
| DeviceRegistryEntry
| undefined;
const area = device
? getDeviceContext(device, this.hass).area
: undefined;
nodes.push({ nodes.push({
id: String(node.node_id), id: String(node.node_id),
name: device?.name_by_user ?? device?.name ?? String(node.node_id), name: device?.name_by_user ?? device?.name ?? String(node.node_id),
context: area?.name,
value: node.is_controller_node ? 3 : node.is_routing ? 2 : 1, value: node.is_controller_node ? 3 : node.is_routing ? 2 : 1,
category: category:
node.status === NodeStatus.Dead node.status === NodeStatus.Dead

View File

@@ -82,7 +82,7 @@ class DialogLabelDetail
this.hass, this.hass,
this._params.entry this._params.entry
? this._params.entry.name || this._params.entry.label_id ? this._params.entry.name || this._params.entry.label_id
: this.hass!.localize("ui.dialogs.label-detail.new_label") : this.hass!.localize("ui.panel.config.labels.detail.new_label")
)} )}
> >
<div> <div>
@@ -95,9 +95,11 @@ class DialogLabelDetail
.value=${this._name} .value=${this._name}
.configValue=${"name"} .configValue=${"name"}
@input=${this._input} @input=${this._input}
.label=${this.hass!.localize("ui.dialogs.label-detail.name")} .label=${this.hass!.localize(
"ui.panel.config.labels.detail.name"
)}
.validationMessage=${this.hass!.localize( .validationMessage=${this.hass!.localize(
"ui.dialogs.label-detail.required_error_msg" "ui.panel.config.labels.detail.required_error_msg"
)} )}
required required
></ha-textfield> ></ha-textfield>
@@ -106,21 +108,25 @@ class DialogLabelDetail
.hass=${this.hass} .hass=${this.hass}
.configValue=${"icon"} .configValue=${"icon"}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.label=${this.hass!.localize("ui.dialogs.label-detail.icon")} .label=${this.hass!.localize(
"ui.panel.config.labels.detail.icon"
)}
></ha-icon-picker> ></ha-icon-picker>
<ha-color-picker <ha-color-picker
.value=${this._color} .value=${this._color}
.configValue=${"color"} .configValue=${"color"}
.hass=${this.hass} .hass=${this.hass}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.label=${this.hass!.localize("ui.dialogs.label-detail.color")} .label=${this.hass!.localize(
"ui.panel.config.labels.detail.color"
)}
></ha-color-picker> ></ha-color-picker>
<ha-textarea <ha-textarea
.value=${this._description} .value=${this._description}
.configValue=${"description"} .configValue=${"description"}
@input=${this._input} @input=${this._input}
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.label-detail.description" "ui.panel.config.labels.detail.description"
)} )}
></ha-textarea> ></ha-textarea>
</div> </div>
@@ -134,7 +140,7 @@ class DialogLabelDetail
@click=${this._deleteEntry} @click=${this._deleteEntry}
.disabled=${this._submitting} .disabled=${this._submitting}
> >
${this.hass!.localize("ui.common.delete")} ${this.hass!.localize("ui.panel.config.labels.detail.delete")}
</ha-button> </ha-button>
` `
: nothing} : nothing}
@@ -144,8 +150,8 @@ class DialogLabelDetail
.disabled=${this._submitting || !this._name} .disabled=${this._submitting || !this._name}
> >
${this._params.entry ${this._params.entry
? this.hass!.localize("ui.common.update") ? this.hass!.localize("ui.panel.config.labels.detail.update")
: this.hass!.localize("ui.common.create")} : this.hass!.localize("ui.panel.config.labels.detail.create")}
</ha-button> </ha-button>
</ha-dialog> </ha-dialog>
`; `;

View File

@@ -415,7 +415,7 @@ class ErrorLogCard extends LitElement {
const downloadUrl = const downloadUrl =
this.provider && this.provider !== "core" this.provider && this.provider !== "core"
? getHassioLogDownloadUrl(this.provider) ? getHassioLogDownloadUrl(this.provider)
: getErrorLogDownloadUrl(this.hass); : getErrorLogDownloadUrl;
const logFileName = const logFileName =
this.provider && this.provider !== "core" this.provider && this.provider !== "core"
? `${this.provider}_${timeString}.log` ? `${this.provider}_${timeString}.log`

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