diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 7d056f13fd57..2db287bfd4d4 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -24,6 +24,7 @@ body:
required: true
- label: I have tried a different browser to see if it is related to my browser.
required: true
+ - label: I have tried reproducing the issue in [safe mode](https://www.home-assistant.io/blog/2023/11/01/release-202311/#restarting-into-safe-mode) to rule out problems with unsupported custom resources.
- type: markdown
attributes:
value: |
diff --git a/.github/workflows/cast_deployment.yaml b/.github/workflows/cast_deployment.yaml
index da5a9c89d354..2aa9ce5168e0 100644
--- a/.github/workflows/cast_deployment.yaml
+++ b/.github/workflows/cast_deployment.yaml
@@ -26,7 +26,7 @@ jobs:
ref: dev
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -62,7 +62,7 @@ jobs:
ref: master
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 607e26f29e20..af8a33890396 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,14 +57,14 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Build resources
- run: ./node_modules/.bin/gulp build-translations build-locale-data
+ run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data
- name: Run Tests
run: yarn run test
build:
@@ -75,7 +75,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -99,7 +99,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index b1fbba90483f..d4d15cc4bcdd 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ uses: github/codeql-action/autobuild@v3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/demo_deployment.yaml b/.github/workflows/demo_deployment.yaml
index 5a4f6f2ccee6..9aa8219845bf 100644
--- a/.github/workflows/demo_deployment.yaml
+++ b/.github/workflows/demo_deployment.yaml
@@ -27,7 +27,7 @@ jobs:
ref: dev
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -63,7 +63,7 @@ jobs:
ref: master
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
diff --git a/.github/workflows/design_deployment.yaml b/.github/workflows/design_deployment.yaml
index 9ab9820fb668..7fd0110210e4 100644
--- a/.github/workflows/design_deployment.yaml
+++ b/.github/workflows/design_deployment.yaml
@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
diff --git a/.github/workflows/design_preview.yaml b/.github/workflows/design_preview.yaml
index d26178ba9f2a..b24148d66ddc 100644
--- a/.github/workflows/design_preview.yaml
+++ b/.github/workflows/design_preview.yaml
@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml
index cbff00ba5707..4ff941bb8ac2 100644
--- a/.github/workflows/nightly.yaml
+++ b/.github/workflows/nightly.yaml
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index a47c5325d085..8bded3fcc5e2 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
- uses: actions/setup-node@v4.0.0
+ uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
diff --git a/build-scripts/bundle.cjs b/build-scripts/bundle.cjs
index 18731af3d5ec..c341909b19b0 100644
--- a/build-scripts/bundle.cjs
+++ b/build-scripts/bundle.cjs
@@ -1,6 +1,7 @@
const path = require("path");
const env = require("./env.cjs");
const paths = require("./paths.cjs");
+const { dependencies } = require("../package.json");
// GitHub base URL to use for production source maps
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
@@ -90,7 +91,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
"@babel/preset-env",
{
useBuiltIns: latestBuild ? false : "usage",
- corejs: latestBuild ? false : "3.33",
+ corejs: latestBuild ? false : dependencies["core-js"],
bugfixes: true,
shippedProposals: true,
},
@@ -140,7 +141,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
// Import helpers and regenerator from runtime package
[
"@babel/plugin-transform-runtime",
- { version: require("../package.json").dependencies["@babel/runtime"] },
+ { version: dependencies["@babel/runtime"] },
],
// Support some proposals still in TC39 process
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
diff --git a/gallery/src/data/demo_states.js b/gallery/src/data/demo_states.js
index 060ce56e29ad..d57cc1a422b0 100644
--- a/gallery/src/data/demo_states.js
+++ b/gallery/src/data/demo_states.js
@@ -509,7 +509,7 @@ export default {
away_mode: "on",
aux_heat: "off",
unit_of_measurement: "°C",
- friendly_name: "Hvac",
+ friendly_name: "HVAC",
supported_features: 3833,
},
last_changed: "2018-07-19T10:44:46.200650+00:00",
diff --git a/gallery/src/pages/lovelace/thermostat-card.ts b/gallery/src/pages/lovelace/thermostat-card.ts
index cc33346a0eb6..623adf5e3436 100644
--- a/gallery/src/pages/lovelace/thermostat-card.ts
+++ b/gallery/src/pages/lovelace/thermostat-card.ts
@@ -35,6 +35,18 @@ const ENTITIES = [
friendly_name: "Nest",
supported_features: 43,
}),
+ getEntity("climate", "sensibo", "fan_only", {
+ current_temperature: null,
+ temperature: null,
+ min_temp: 0,
+ max_temp: 1,
+ target_temp_step: 1,
+ hvac_modes: ["fan_only", "off"],
+ friendly_name: "Sensibo purifier",
+ fan_modes: ["low", "high"],
+ fan_mode: "low",
+ supported_features: 9,
+ }),
getEntity("climate", "unavailable", "unavailable", {
supported_features: 43,
}),
@@ -57,6 +69,23 @@ const CONFIGS = [
entity: climate.nest
`,
},
+ {
+ heading: "Fan only example",
+ config: `
+- type: thermostat
+ entity: climate.sensibo
+ features:
+ - type: climate-hvac-modes
+ hvac_modes:
+ - fan_only
+ - 'off'
+ - type: climate-fan-modes
+ style: icons
+ fan_modes:
+ - low
+ - high
+ `,
+ },
{
heading: "Unavailable",
config: `
diff --git a/gallery/src/pages/more-info/climate.ts b/gallery/src/pages/more-info/climate.ts
index daa3554c5035..f6216a55b1c4 100644
--- a/gallery/src/pages/more-info/climate.ts
+++ b/gallery/src/pages/more-info/climate.ts
@@ -31,6 +31,21 @@ const ENTITIES = [
max_temp: 30,
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
}),
+ getEntity("climate", "fan", "fan_only", {
+ friendly_name: "Basic fan",
+ hvac_modes: ["fan_only", "off"],
+ hvac_mode: "fan_only",
+ fan_modes: ["low", "high"],
+ fan_mode: "low",
+ current_temperature: null,
+ temperature: null,
+ min_temp: 0,
+ max_temp: 1,
+ target_temp_step: 1,
+ supported_features:
+ // eslint-disable-next-line no-bitwise
+ ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE,
+ }),
getEntity("climate", "hvac", "auto", {
friendly_name: "Basic hvac",
hvac_modes: ["auto", "off"],
diff --git a/gallery/src/pages/more-info/update.ts b/gallery/src/pages/more-info/update.ts
index ce4fa4f6f73d..43951d4c5e8a 100644
--- a/gallery/src/pages/more-info/update.ts
+++ b/gallery/src/pages/more-info/update.ts
@@ -1,12 +1,6 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
-import {
- UPDATE_SUPPORT_BACKUP,
- UPDATE_SUPPORT_PROGRESS,
- UPDATE_SUPPORT_INSTALL,
- UPDATE_SUPPORT_RELEASE_NOTES,
-} from "../../../../src/data/update";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import {
@@ -15,13 +9,14 @@ import {
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
import { LONG_TEXT } from "../../data/text";
+import { UpdateEntityFeature } from "../../../../src/data/update";
const base_attributes = {
title: "Awesome",
installed_version: "1.2.2",
latest_version: "1.2.3",
release_url: "https://home-assistant.io",
- supported_features: UPDATE_SUPPORT_INSTALL,
+ supported_features: UpdateEntityFeature.INSTALL,
skipped_version: null,
in_progress: false,
release_summary:
@@ -61,7 +56,7 @@ const ENTITIES = [
getEntity("update", "update7", "on", {
...base_attributes,
supported_features:
- base_attributes.supported_features + UPDATE_SUPPORT_BACKUP,
+ base_attributes.supported_features + UpdateEntityFeature.BACKUP,
friendly_name: "With backup support",
}),
getEntity("update", "update8", "on", {
@@ -73,21 +68,21 @@ const ENTITIES = [
...base_attributes,
in_progress: 25,
supported_features:
- base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
+ base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
friendly_name: "With 25 in_progress",
}),
getEntity("update", "update10", "on", {
...base_attributes,
in_progress: 50,
supported_features:
- base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
+ base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
friendly_name: "With 50 in_progress",
}),
getEntity("update", "update11", "on", {
...base_attributes,
in_progress: 75,
supported_features:
- base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
+ base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
friendly_name: "With 75 in_progress",
}),
getEntity("update", "update12", "unavailable", {
@@ -114,19 +109,19 @@ const ENTITIES = [
...base_attributes,
friendly_name: "Update with release notes",
supported_features:
- base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
+ base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES,
}),
getEntity("update", "update17", "off", {
...base_attributes,
friendly_name: "Update with release notes error",
supported_features:
- base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
+ base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES,
}),
getEntity("update", "update18", "off", {
...base_attributes,
friendly_name: "Update with release notes loading",
supported_features:
- base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
+ base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES,
}),
getEntity("update", "update19", "on", {
...base_attributes,
@@ -142,9 +137,10 @@ const ENTITIES = [
getEntity("update", "update21", "on", {
...base_attributes,
in_progress: true,
- friendly_name: "Update with in_progress true and UPDATE_SUPPORT_PROGRESS",
+ friendly_name:
+ "Update with in_progress true and UpdateEntityFeature.PROGRESS",
supported_features:
- base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
+ base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
}),
];
diff --git a/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts b/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts
index 8a1c3c670221..4dcfde2ec2b4 100644
--- a/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts
+++ b/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
-import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-select";
import {
extractApiErrorMessage,
diff --git a/package.json b/package.json
index 540059eeb516..663a412014d2 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
- "@babel/runtime": "7.23.5",
+ "@babel/runtime": "7.23.7",
"@braintree/sanitize-url": "7.0.0",
"@codemirror/autocomplete": "6.11.1",
"@codemirror/commands": "6.3.2",
@@ -33,7 +33,7 @@
"@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.5",
"@codemirror/state": "6.3.3",
- "@codemirror/view": "6.22.2",
+ "@codemirror/view": "6.22.3",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.0",
"@formatjs/intl-displaynames": "6.6.4",
@@ -80,9 +80,9 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
- "@material/web": "=1.0.1",
- "@mdi/js": "7.3.67",
- "@mdi/svg": "7.3.67",
+ "@material/web": "=1.1.1",
+ "@mdi/js": "7.4.47",
+ "@mdi/svg": "7.4.47",
"@polymer/paper-input": "3.2.1",
"@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1",
@@ -90,8 +90,8 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
- "@vaadin/combo-box": "24.2.5",
- "@vaadin/vaadin-themable-mixin": "24.2.5",
+ "@vaadin/combo-box": "24.3.2",
+ "@vaadin/vaadin-themable-mixin": "24.3.2",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -101,16 +101,16 @@
"app-datepicker": "5.1.1",
"chart.js": "4.4.1",
"comlink": "4.4.1",
- "core-js": "3.33.3",
+ "core-js": "3.34.0",
"cropperjs": "1.6.1",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
- "element-internals-polyfill": "1.3.9",
+ "element-internals-polyfill": "1.3.10",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
- "hls.js": "1.4.13",
+ "hls.js": "1.4.14",
"home-assistant-js-websocket": "9.1.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.8",
@@ -119,7 +119,7 @@
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.4.4",
- "marked": "11.0.1",
+ "marked": "11.1.0",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@@ -138,7 +138,7 @@
"unfetch": "5.0.0",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
- "vue": "2.7.15",
+ "vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
"workbox-cacheable-response": "7.0.0",
@@ -150,13 +150,13 @@
"xss": "1.0.14"
},
"devDependencies": {
- "@babel/core": "7.23.5",
- "@babel/helper-define-polyfill-provider": "0.4.3",
- "@babel/plugin-proposal-decorators": "7.23.5",
- "@babel/plugin-transform-runtime": "7.23.4",
- "@babel/preset-env": "7.23.5",
+ "@babel/core": "7.23.7",
+ "@babel/helper-define-polyfill-provider": "0.4.4",
+ "@babel/plugin-proposal-decorators": "7.23.7",
+ "@babel/plugin-transform-runtime": "7.23.7",
+ "@babel/preset-env": "7.23.7",
"@babel/preset-typescript": "7.23.3",
- "@bundle-stats/plugin-webpack-filter": "4.8.3",
+ "@bundle-stats/plugin-webpack-filter": "4.8.4",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.1.0",
"@octokit/auth-oauth-device": "6.0.1",
@@ -165,7 +165,7 @@
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "25.0.7",
- "@rollup/plugin-json": "6.0.1",
+ "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -184,22 +184,22 @@
"@types/tar": "6.1.10",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
- "@typescript-eslint/eslint-plugin": "6.13.2",
- "@typescript-eslint/parser": "6.13.2",
+ "@typescript-eslint/eslint-plugin": "6.16.0",
+ "@typescript-eslint/parser": "6.16.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0",
- "chai": "4.3.10",
+ "chai": "5.0.0",
"del": "7.1.0",
- "eslint": "8.55.0",
+ "eslint": "8.56.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-disable": "2.0.3",
- "eslint-plugin-import": "2.29.0",
- "eslint-plugin-lit": "1.10.1",
+ "eslint-plugin-import": "2.29.1",
+ "eslint-plugin-lit": "1.11.0",
"eslint-plugin-lit-a11y": "4.1.1",
"eslint-plugin-unused-imports": "3.0.0",
"eslint-plugin-wc": "2.0.4",
@@ -217,25 +217,25 @@
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "15.2.0",
- "lit-analyzer": "2.0.1",
+ "lit-analyzer": "2.0.2",
"lodash.template": "4.5.0",
"magic-string": "0.30.5",
"map-stream": "0.0.7",
"mocha": "10.2.0",
"object-hash": "3.0.0",
- "open": "9.1.0",
+ "open": "10.0.2",
"pinst": "3.0.0",
"prettier": "3.1.1",
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
- "rollup-plugin-visualizer": "5.11.0",
+ "rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.5",
"sinon": "17.0.1",
"source-map-url": "0.4.1",
"systemjs": "6.14.2",
"tar": "6.2.0",
- "terser-webpack-plugin": "5.3.9",
+ "terser-webpack-plugin": "5.3.10",
"ts-lit-plugin": "2.0.1",
"typescript": "5.3.3",
"vinyl-buffer": "1.0.1",
@@ -245,7 +245,7 @@
"webpack-dev-server": "4.15.1",
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
- "webpackbar": "5.0.2",
+ "webpackbar": "6.0.0",
"workbox-build": "7.0.0"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
diff --git a/pyproject.toml b/pyproject.toml
index afea7e45d6b0..7d09a1121ede 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
-version = "20231208.2"
+version = "20240102.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
diff --git a/src/common/const.ts b/src/common/const.ts
index 66c14e64e140..68f4fa97b1c1 100644
--- a/src/common/const.ts
+++ b/src/common/const.ts
@@ -29,6 +29,7 @@ import {
mdiFlash,
mdiFlower,
mdiFormatListBulleted,
+ mdiFormatListCheckbox,
mdiFormTextbox,
mdiGauge,
mdiGoogleAssistant,
@@ -64,6 +65,7 @@ import {
mdiTransmissionTower,
mdiWater,
mdiWaterPercent,
+ mdiWeatherPartlyCloudy,
mdiWeatherPouring,
mdiWeatherRainy,
mdiWeatherWindy,
@@ -128,6 +130,7 @@ export const FIXED_DOMAIN_ICONS = {
updater: mdiCloudUpload,
vacuum: mdiRobotVacuum,
wake_word: mdiChatSleep,
+ weather: mdiWeatherPartlyCloudy,
zone: mdiMapMarkerRadius,
};
@@ -166,6 +169,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
precipitation_intensity: mdiWeatherPouring,
pressure: mdiGauge,
reactive_power: mdiFlash,
+ shopping_List: mdiFormatListCheckbox,
signal_strength: mdiWifi,
sound_pressure: mdiEarHearing,
speed: mdiSpeedometer,
diff --git a/src/common/datetime/localize_date.ts b/src/common/datetime/localize_date.ts
new file mode 100644
index 000000000000..428261801d58
--- /dev/null
+++ b/src/common/datetime/localize_date.ts
@@ -0,0 +1,31 @@
+import memoizeOne from "memoize-one";
+
+export const localizeWeekdays = memoizeOne(
+ (language: string, short: boolean): string[] => {
+ const days: string[] = [];
+ const format = new Intl.DateTimeFormat(language, {
+ weekday: short ? "short" : "long",
+ timeZone: "UTC",
+ });
+ for (let i = 0; i < 7; i++) {
+ const date = new Date(Date.UTC(1970, 0, 1 + 3 + i));
+ days.push(format.format(date));
+ }
+ return days;
+ }
+);
+
+export const localizeMonths = memoizeOne(
+ (language: string, short: boolean): string[] => {
+ const months: string[] = [];
+ const format = new Intl.DateTimeFormat(language, {
+ month: short ? "short" : "long",
+ timeZone: "UTC",
+ });
+ for (let i = 0; i < 12; i++) {
+ const date = new Date(Date.UTC(1970, 0 + i, 1));
+ months.push(format.format(date));
+ }
+ return months;
+ }
+);
diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts
index 565358e9e409..dfbbc69a5e17 100644
--- a/src/common/entity/domain_icon.ts
+++ b/src/common/entity/domain_icon.ts
@@ -28,10 +28,12 @@ import {
mdiLockAlert,
mdiLockClock,
mdiLockOpen,
+ mdiMeterGas,
mdiMotionSensor,
mdiPackage,
mdiPackageDown,
mdiPackageUp,
+ mdiPipeValve,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
@@ -274,6 +276,16 @@ export const domainIconWithoutDefault = (
: mdiPackageUp
: mdiPackage;
+ case "valve":
+ switch (stateObj?.attributes.device_class) {
+ case "water":
+ return mdiPipeValve;
+ case "gas":
+ return mdiMeterGas;
+ default:
+ return mdiPipeValve;
+ }
+
case "water_heater":
return compareState === "off" ? mdiWaterBoilerOff : mdiWaterBoiler;
diff --git a/src/common/entity/state_active.ts b/src/common/entity/state_active.ts
index 7c304f03890e..16ca41c9c19f 100644
--- a/src/common/entity/state_active.ts
+++ b/src/common/entity/state_active.ts
@@ -42,6 +42,8 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
return compareState !== "standby";
case "vacuum":
return !["idle", "docked", "paused"].includes(compareState);
+ case "valve":
+ return compareState !== "closed";
case "plant":
return compareState === "problem";
case "group":
diff --git a/src/common/entity/state_color.ts b/src/common/entity/state_color.ts
index 0abaca4c17c8..09145eed83c7 100644
--- a/src/common/entity/state_color.ts
+++ b/src/common/entity/state_color.ts
@@ -37,6 +37,7 @@ const STATE_COLORED_DOMAIN = new Set([
"timer",
"update",
"vacuum",
+ "valve",
"water_heater",
]);
diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts
index 1914849be492..a0fc6ca53ba5 100644
--- a/src/components/chart/statistics-chart.ts
+++ b/src/components/chart/statistics-chart.ts
@@ -101,7 +101,8 @@ export class StatisticsChart extends LitElement {
changedProps.has("unit") ||
changedProps.has("period") ||
changedProps.has("chartType") ||
- changedProps.has("logarithmicScale")
+ changedProps.has("logarithmicScale") ||
+ changedProps.has("hideLegend")
) {
this._createOptions();
}
diff --git a/src/components/date-range-picker.ts b/src/components/date-range-picker.ts
index b60ce14763e3..237576a89ce3 100644
--- a/src/components/date-range-picker.ts
+++ b/src/components/date-range-picker.ts
@@ -5,6 +5,10 @@ import DateRangePicker from "vue2-daterange-picker";
// @ts-ignore
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
import { fireEvent } from "../common/dom/fire_event";
+import {
+ localizeWeekdays,
+ localizeMonths,
+} from "../common/datetime/localize_date";
// Set the current date to the left picker instead of the right picker because the right is hidden
const CustomDateRangePicker = Vue.extend({
@@ -63,6 +67,10 @@ const Component = Vue.extend({
type: Boolean,
default: false,
},
+ language: {
+ type: String,
+ default: "en",
+ },
},
render(createElement) {
// @ts-expect-error
@@ -77,6 +85,8 @@ const Component = Vue.extend({
ranges: this.ranges ? {} : false,
"locale-data": {
firstDay: this.firstDay,
+ daysOfWeek: localizeWeekdays(this.language, true),
+ monthNames: localizeMonths(this.language, false),
},
},
model: {
@@ -145,6 +155,8 @@ class DateRangePickerElement extends WrappedElement {
);
color: var(--primary-text-color);
min-width: initial !important;
+ max-height: var(--date-range-picker-max-height);
+ overflow-y: auto;
}
.daterangepicker:before {
display: none;
@@ -162,7 +174,7 @@ class DateRangePickerElement extends WrappedElement {
color: var(--secondary-text-color);
border-radius: 0;
outline: none;
- width: 32px;
+ min-width: 32px;
height: 32px;
}
.daterangepicker td.off,
@@ -238,6 +250,9 @@ class DateRangePickerElement extends WrappedElement {
}
.daterangepicker .drp-calendar.left {
padding: 8px;
+ width: unset;
+ max-width: unset;
+ min-width: 270px;
}
.daterangepicker.show-calendar .ranges {
margin-top: 0;
diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts
index d2a0f689fb75..423de3df7a88 100644
--- a/src/components/ha-area-picker.ts
+++ b/src/components/ha-area-picker.ts
@@ -446,6 +446,7 @@ export class HaAreaPicker extends LitElement {
cancel: () => {
this._setValue(undefined);
this._suggestion = undefined;
+ this.comboBox.setInputValue("");
},
});
}
diff --git a/src/components/ha-big-number.ts b/src/components/ha-big-number.ts
index ee0a5cf75977..85a7783c87c1 100644
--- a/src/components/ha-big-number.ts
+++ b/src/components/ha-big-number.ts
@@ -86,6 +86,7 @@ export class HaBigNumber extends LitElement {
.value .decimal {
font-size: 0.42em;
line-height: 1.33;
+ min-height: 1.33em;
}
.value .unit {
font-size: 0.33em;
diff --git a/src/components/ha-check-list-item.ts b/src/components/ha-check-list-item.ts
index 96e62a475eae..19f04feda9cd 100644
--- a/src/components/ha-check-list-item.ts
+++ b/src/components/ha-check-list-item.ts
@@ -3,9 +3,15 @@ import { CheckListItemBase } from "@material/mwc-list/mwc-check-list-item-base";
import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css";
import { styles } from "@material/mwc-list/mwc-list-item.css";
import { customElement } from "lit/decorators";
+import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-check-list-item")
export class HaCheckListItem extends CheckListItemBase {
+ async onChange(event) {
+ super.onChange(event);
+ fireEvent(this, event.type);
+ }
+
static override styles = [
styles,
controlStyles,
@@ -22,6 +28,15 @@ export class HaCheckListItem extends CheckListItemBase {
margin-inline-start: 0px;
direction: var(--direction);
}
+ .mdc-deprecated-list-item__meta {
+ flex-shrink: 0;
+ direction: var(--direction);
+ margin-inline-start: auto;
+ margin-inline-end: 0;
+ }
+ .mdc-deprecated-list-item__graphic {
+ margin-top: var(--check-list-item-graphic-margin-top);
+ }
`,
];
}
diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts
index 9c50d0a4bbb2..ce6d54f41565 100644
--- a/src/components/ha-combo-box.ts
+++ b/src/components/ha-combo-box.ts
@@ -180,7 +180,7 @@ export class HaComboBox extends LitElement {
>`}
.icon=${this.icon}
.invalid=${this.invalid}
- helper=${ifDefined(this.helper)}
+ .helper=${this.helper}
helperPersistent
>
diff --git a/src/components/ha-date-input.ts b/src/components/ha-date-input.ts
index d3b5a0607eb3..d4d9922639d9 100644
--- a/src/components/ha-date-input.ts
+++ b/src/components/ha-date-input.ts
@@ -18,7 +18,8 @@ export interface datePickerDialogParams {
max?: string;
locale?: string;
firstWeekday?: number;
- onChange: (value: string) => void;
+ canClear?: boolean;
+ onChange: (value: string | undefined) => void;
}
const showDatePickerDialog = (
@@ -49,6 +50,8 @@ export class HaDateInput extends LitElement {
@property() public helper?: string;
+ @property({ type: Boolean }) public canClear?: boolean;
+
render() {
return html` this._valueChanged(value),
locale: this.locale.language,
firstWeekday: firstWeekdayIndex(this.locale),
});
}
- private _valueChanged(value: string) {
+ private _keyDown(ev: KeyboardEvent) {
+ if (!this.canClear) {
+ return;
+ }
+ if (["Backspace", "Delete"].includes(ev.key)) {
+ this._valueChanged(undefined);
+ }
+ }
+
+ private _valueChanged(value: string | undefined) {
if (this.value !== value) {
this.value = value;
fireEvent(this, "change");
diff --git a/src/components/ha-date-range-picker.ts b/src/components/ha-date-range-picker.ts
index 29b43c697918..7e16fecd22db 100644
--- a/src/components/ha-date-range-picker.ts
+++ b/src/components/ha-date-range-picker.ts
@@ -253,6 +253,7 @@ export class HaDateRangePicker extends LitElement {
opening-direction=${this.openingDirection ||
this._calcedOpeningDirection}
first-day=${firstWeekdayIndex(this.hass.locale)}
+ language=${this.hass.locale.language}
>
${!this.minimal
diff --git a/src/components/ha-dialog-date-picker.ts b/src/components/ha-dialog-date-picker.ts
index 2422754cfb15..b71e063ae410 100644
--- a/src/components/ha-dialog-date-picker.ts
+++ b/src/components/ha-dialog-date-picker.ts
@@ -50,6 +50,15 @@ export class HaDialogDatePicker extends LitElement {
@datepicker-value-updated=${this._valueChanged}
.firstDayOfWeek=${this._params.firstWeekday}
>
+ ${this._params.canClear
+ ? html`
+ ${this.hass.localize("ui.dialogs.date-picker.clear")}
+ `
+ : nothing}
${this.hass.localize("ui.dialogs.date-picker.today")}
@@ -66,6 +75,11 @@ export class HaDialogDatePicker extends LitElement {
this._value = ev.detail.value;
}
+ private _clear() {
+ this._params?.onChange(undefined);
+ this.closeDialog();
+ }
+
private _setToday() {
const today = new Date();
this._value = format(today, "yyyy-MM-dd");
diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts
index feaceec93daf..9cf199dbe52b 100644
--- a/src/components/ha-dialog.ts
+++ b/src/components/ha-dialog.ts
@@ -13,13 +13,15 @@ export const createCloseHeading = (
hass: HomeAssistant | undefined,
title: string | TemplateResult
) => html`
-
-
+
`;
@customElement("ha-dialog")
@@ -94,15 +96,12 @@ export class HaDialog extends DialogBase {
}
.mdc-dialog__title {
padding: 24px 24px 0 24px;
- text-overflow: ellipsis;
- overflow: hidden;
}
.mdc-dialog__actions {
padding: 12px 24px 12px 24px;
}
.mdc-dialog__title::before {
- display: block;
- height: 0px;
+ content: unset;
}
.mdc-dialog .mdc-dialog__content {
position: var(--dialog-content-position, relative);
@@ -126,19 +125,26 @@ export class HaDialog extends DialogBase {
flex-direction: column;
}
.header_title {
- margin-right: 32px;
- margin-inline-end: 32px;
- margin-inline-start: initial;
+ position: relative;
+ padding-right: 40px;
+ padding-inline-end: 40px;
+ padding-inline-start: initial;
direction: var(--direction);
}
+ .header_title span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: block;
+ }
.header_button {
position: absolute;
- right: 16px;
- top: 14px;
+ right: -8px;
+ top: -8px;
text-decoration: none;
color: inherit;
inset-inline-start: initial;
- inset-inline-end: 16px;
+ inset-inline-end: -8px;
direction: var(--direction);
}
.dialog-actions {
diff --git a/src/components/ha-labeled-slider.ts b/src/components/ha-labeled-slider.ts
index 366eb4f5555c..5faf7a8b96b2 100644
--- a/src/components/ha-labeled-slider.ts
+++ b/src/components/ha-labeled-slider.ts
@@ -10,9 +10,9 @@ class HaLabeledSlider extends LitElement {
@property() public caption?: string;
- @property() public disabled?: boolean;
+ @property({ type: Boolean }) public disabled = false;
- @property() public required?: boolean;
+ @property({ type: Boolean }) public required = true;
@property() public min: number = 0;
diff --git a/src/components/ha-list-item.ts b/src/components/ha-list-item.ts
index a5179008b5b5..dcd13f0f7074 100644
--- a/src/components/ha-list-item.ts
+++ b/src/components/ha-list-item.ts
@@ -36,16 +36,24 @@ export class HaListItem extends ListItemBase {
--mdc-list-item-graphic-margin,
16px
) !important;
- direction: var(--direction);
+ direction: var(--direction) !important;
}
span.material-icons:last-of-type {
margin-inline-start: auto !important;
margin-inline-end: 0px !important;
- direction: var(--direction);
+ direction: var(--direction) !important;
}
.mdc-deprecated-list-item__meta {
display: var(--mdc-list-item-meta-display);
align-items: center;
+ flex-shrink: 0;
+ }
+ :host([graphic="icon"]:not([twoline]))
+ .mdc-deprecated-list-item__graphic {
+ margin-inline-end: var(
+ --mdc-list-item-graphic-margin,
+ 20px
+ ) !important;
}
:host([multiline-secondary]) {
height: auto;
@@ -78,6 +86,15 @@ export class HaListItem extends ListItemBase {
pointer-events: unset;
}
`,
+ // safari workaround - must be explicit
+ document.dir === "rtl"
+ ? css`
+ span.material-icons:first-of-type,
+ span.material-icons:last-of-type {
+ direction: rtl !important;
+ }
+ `
+ : css``,
];
}
}
diff --git a/src/components/ha-markdown-element.ts b/src/components/ha-markdown-element.ts
index e00bfceb9477..0e806b7c7dcf 100644
--- a/src/components/ha-markdown-element.ts
+++ b/src/components/ha-markdown-element.ts
@@ -95,6 +95,15 @@ class HaMarkdownElement extends ReactiveElement {
}
node.firstElementChild!.replaceWith(alertNote);
}
+ } else if (
+ node instanceof HTMLElement &&
+ ["ha-alert", "ha-qr-code", "ha-icon", "ha-svg-icon"].includes(
+ node.localName
+ )
+ ) {
+ import(
+ /* webpackInclude: /(ha-alert)|(ha-qr-code)|(ha-icon)|(ha-svg-icon)/ */ `./${node.localName}`
+ );
}
}
}
diff --git a/src/components/ha-markdown.ts b/src/components/ha-markdown.ts
index c248a320e819..f81ddd286037 100644
--- a/src/components/ha-markdown.ts
+++ b/src/components/ha-markdown.ts
@@ -2,11 +2,6 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-markdown-element";
-// Import components that are allwoed to be defined.
-import "./ha-alert";
-import "./ha-icon";
-import "./ha-svg-icon";
-
@customElement("ha-markdown")
export class HaMarkdown extends LitElement {
@property() public content?;
diff --git a/src/components/ha-qr-code.ts b/src/components/ha-qr-code.ts
new file mode 100644
index 000000000000..f6a686d06823
--- /dev/null
+++ b/src/components/ha-qr-code.ts
@@ -0,0 +1,114 @@
+import { LitElement, PropertyValues, css, html, nothing } from "lit";
+import { customElement, property, query, state } from "lit/decorators";
+import QRCode from "qrcode";
+
+@customElement("ha-qr-code")
+export class HaQrCode extends LitElement {
+ @property() public data?: string;
+
+ @property({ attribute: "error-correction-level" })
+ public errorCorrectionLevel: "low" | "medium" | "quartile" | "high" =
+ "medium";
+
+ @property({ type: Number })
+ public width = 4;
+
+ @property({ type: Number })
+ public scale = 4;
+
+ @property({ type: Number })
+ public margin = 4;
+
+ @property({ type: Number }) public maskPattern?:
+ | 0
+ | 1
+ | 2
+ | 3
+ | 4
+ | 5
+ | 6
+ | 7;
+
+ @property({ attribute: "center-image" }) public centerImage?: string;
+
+ @state() private _error?: string;
+
+ @query("canvas") private _canvas?: HTMLCanvasElement;
+
+ protected willUpdate(changedProperties: PropertyValues): void {
+ super.willUpdate(changedProperties);
+ if (
+ (changedProperties.has("data") ||
+ changedProperties.has("scale") ||
+ changedProperties.has("width") ||
+ changedProperties.has("margin") ||
+ changedProperties.has("maskPattern") ||
+ changedProperties.has("errorCorrectionLevel")) &&
+ this._error
+ ) {
+ this._error = undefined;
+ }
+ }
+
+ updated(changedProperties: PropertyValues) {
+ const canvas = this._canvas;
+ if (
+ canvas &&
+ this.data &&
+ (changedProperties.has("data") ||
+ changedProperties.has("scale") ||
+ changedProperties.has("width") ||
+ changedProperties.has("margin") ||
+ changedProperties.has("maskPattern") ||
+ changedProperties.has("errorCorrectionLevel") ||
+ changedProperties.has("centerImage"))
+ ) {
+ const computedStyles = getComputedStyle(this);
+
+ QRCode.toCanvas(canvas, this.data, {
+ errorCorrectionLevel: this.errorCorrectionLevel,
+ width: this.width,
+ scale: this.scale,
+ margin: this.margin,
+ maskPattern: this.maskPattern,
+ color: {
+ light: computedStyles.getPropertyValue("--card-background-color"),
+ dark: computedStyles.getPropertyValue("--primary-text-color"),
+ },
+ }).catch((err) => {
+ this._error = err.message;
+ });
+
+ if (this.centerImage) {
+ const context = this._canvas!.getContext("2d");
+ const imageObj = new Image();
+ imageObj.src = this.centerImage;
+ imageObj.onload = () => {
+ context?.drawImage(
+ imageObj,
+ canvas.width * 0.375,
+ canvas.height * 0.375,
+ canvas.width / 4,
+ canvas.height / 4
+ );
+ };
+ }
+ }
+ }
+
+ render() {
+ if (!this.data) {
+ return nothing;
+ }
+ if (this._error) {
+ return html`
${this._error} `;
+ }
+ return html`
`;
+ }
+
+ static styles = css`
+ :host {
+ display: block;
+ }
+ `;
+}
diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts
index 3bc9451e1807..1d4d2b98d4d5 100644
--- a/src/components/ha-selector/ha-selector-number.ts
+++ b/src/components/ha-selector/ha-selector-number.ts
@@ -43,6 +43,22 @@ export class HaNumberSelector extends LitElement {
this.selector.number?.min === undefined ||
this.selector.number?.max === undefined;
+ let sliderStep;
+
+ if (!isBox) {
+ sliderStep = this.selector.number!.step ?? 1;
+ if (sliderStep === "any") {
+ sliderStep = 1;
+ // divide the range of the slider by 100 steps
+ const step =
+ (this.selector.number!.max! - this.selector.number!.min!) / 100;
+ // biggest step size is 1, round the step size to a division of 1
+ while (sliderStep > step) {
+ sliderStep /= 10;
+ }
+ }
+ }
+
return html`
${(supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET) ||
- supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_BUTTONS)) &&
+ supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP)) &&
stateActive(stateObj)
? html`
@@ -104,8 +104,9 @@ class MoreInfoMediaPlayer extends LitElement {
: ""}
${supportsFeature(
stateObj,
- MediaPlayerEntityFeature.VOLUME_BUTTONS
- )
+ MediaPlayerEntityFeature.VOLUME_SET
+ ) ||
+ supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP)
? html`
`
: ""}
- ${supportsFeature(this.stateObj!, UPDATE_SUPPORT_RELEASE_NOTES) &&
+ ${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error
? !this._releaseNotes
? html`
@@ -117,7 +113,7 @@ class MoreInfoUpdate extends LitElement {
.content=${this.stateObj.attributes.release_summary}
>`
: ""}
- ${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP)
+ ${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
? html`
`}
- ${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL)
+ ${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
? html`
{
this._releaseNotes = result;
@@ -186,7 +182,7 @@ class MoreInfoUpdate extends LitElement {
}
get _shouldCreateBackup(): boolean | null {
- if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) {
+ if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return null;
}
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
@@ -206,7 +202,7 @@ class MoreInfoUpdate extends LitElement {
}
if (
- supportsFeature(this.stateObj!, UPDATE_SUPPORT_SPECIFIC_VERSION) &&
+ supportsFeature(this.stateObj!, UpdateEntityFeature.SPECIFIC_VERSION) &&
this.stateObj!.attributes.latest_version
) {
installData.version = this.stateObj!.attributes.latest_version;
diff --git a/src/dialogs/more-info/controls/more-info-valve.ts b/src/dialogs/more-info/controls/more-info-valve.ts
new file mode 100644
index 000000000000..f7bae2a9bef5
--- /dev/null
+++ b/src/dialogs/more-info/controls/more-info-valve.ts
@@ -0,0 +1,192 @@
+import { mdiMenu, mdiSwapVertical } from "@mdi/js";
+import {
+ CSSResultGroup,
+ LitElement,
+ PropertyValues,
+ css,
+ html,
+ nothing,
+} from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { supportsFeature } from "../../../common/entity/supports-feature";
+import "../../../components/ha-attributes";
+import "../../../components/ha-icon-button-group";
+import "../../../components/ha-icon-button-toggle";
+import {
+ ValveEntity,
+ ValveEntityFeature,
+ computeValvePositionStateDisplay,
+} from "../../../data/valve";
+import "../../../state-control/valve/ha-state-control-valve-buttons";
+import "../../../state-control/valve/ha-state-control-valve-position";
+import "../../../state-control/valve/ha-state-control-valve-toggle";
+import type { HomeAssistant } from "../../../types";
+import "../components/ha-more-info-state-header";
+import { moreInfoControlStyle } from "../components/more-info-control-style";
+
+type Mode = "position" | "button";
+
+@customElement("more-info-valve")
+class MoreInfoValve extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public stateObj?: ValveEntity;
+
+ @state() private _mode?: Mode;
+
+ private _setMode(ev) {
+ this._mode = ev.currentTarget.mode;
+ }
+
+ protected willUpdate(changedProps: PropertyValues): void {
+ super.willUpdate(changedProps);
+ if (changedProps.has("stateObj") && this.stateObj) {
+ const entityId = this.stateObj.entity_id;
+ const oldEntityId = changedProps.get("stateObj")?.entity_id;
+ if (!this._mode || entityId !== oldEntityId) {
+ this._mode = supportsFeature(
+ this.stateObj,
+ ValveEntityFeature.SET_POSITION
+ )
+ ? "position"
+ : "button";
+ }
+ }
+ }
+
+ private get _stateOverride() {
+ const stateDisplay = this.hass.formatEntityState(this.stateObj!);
+
+ const positionStateDisplay = computeValvePositionStateDisplay(
+ this.stateObj!,
+ this.hass
+ );
+
+ if (positionStateDisplay) {
+ return `${stateDisplay} ⸱ ${positionStateDisplay}`;
+ }
+ return stateDisplay;
+ }
+
+ protected render() {
+ if (!this.hass || !this.stateObj) {
+ return nothing;
+ }
+
+ const supportsPosition = supportsFeature(
+ this.stateObj,
+ ValveEntityFeature.SET_POSITION
+ );
+
+ const supportsOpenClose =
+ supportsFeature(this.stateObj, ValveEntityFeature.OPEN) ||
+ supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) ||
+ supportsFeature(this.stateObj, ValveEntityFeature.STOP);
+
+ const supportsOpenCloseWithoutStop =
+ supportsFeature(this.stateObj, ValveEntityFeature.OPEN) &&
+ supportsFeature(this.stateObj, ValveEntityFeature.CLOSE) &&
+ !supportsFeature(this.stateObj, ValveEntityFeature.STOP);
+
+ return html`
+
+
+
+ ${
+ this._mode === "position"
+ ? html`
+ ${supportsPosition
+ ? html`
+
+ `
+ : nothing}
+ `
+ : nothing
+ }
+ ${
+ this._mode === "button"
+ ? html`
+ ${supportsOpenCloseWithoutStop
+ ? html`
+
+ `
+ : supportsOpenClose
+ ? html`
+
+ `
+ : nothing}
+ `
+ : nothing
+ }
+
+ ${
+ supportsPosition && supportsOpenClose
+ ? html`
+
+
+
+
+ `
+ : nothing
+ }
+
+
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return [
+ moreInfoControlStyle,
+ css`
+ .main-control {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+ .main-control > * {
+ margin: 0 8px;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "more-info-valve": MoreInfoValve;
+ }
+}
diff --git a/src/dialogs/more-info/state_more_info_control.ts b/src/dialogs/more-info/state_more_info_control.ts
index 65a2e0a1a0d4..2be9b0877b89 100644
--- a/src/dialogs/more-info/state_more_info_control.ts
+++ b/src/dialogs/more-info/state_more_info_control.ts
@@ -35,6 +35,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
timer: () => import("./controls/more-info-timer"),
update: () => import("./controls/more-info-update"),
vacuum: () => import("./controls/more-info-vacuum"),
+ valve: () => import("./controls/more-info-valve"),
water_heater: () => import("./controls/more-info-water_heater"),
weather: () => import("./controls/more-info-weather"),
};
diff --git a/src/dialogs/update_backup/dialog-update-backup.ts b/src/dialogs/update_backup/dialog-update-backup.ts
new file mode 100644
index 000000000000..5caa9a58417b
--- /dev/null
+++ b/src/dialogs/update_backup/dialog-update-backup.ts
@@ -0,0 +1,92 @@
+import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { fireEvent } from "../../common/dom/fire_event";
+import "../../components/ha-button";
+import { createCloseHeading } from "../../components/ha-dialog";
+import { HomeAssistant } from "../../types";
+import { UpdateBackupDialogParams } from "./show-update-backup-dialog";
+
+@customElement("dialog-update-backup")
+class DialogBox extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @state() private _params?: UpdateBackupDialogParams;
+
+ public async showDialog(params: UpdateBackupDialogParams): Promise
{
+ this._params = params;
+ }
+
+ protected render() {
+ if (!this._params) {
+ return nothing;
+ }
+
+ return html`
+
+ ${this.hass.localize("ui.dialogs.update_backup.text")}
+
+ ${this.hass!.localize("ui.common.no")}
+
+
+ ${this.hass.localize("ui.dialogs.update_backup.create")}
+
+
+ `;
+ }
+
+ private _no(): void {
+ if (this._params!.submit) {
+ this._params!.submit(false);
+ }
+ this.closeDialog();
+ }
+
+ private _yes(): void {
+ if (this._params!.submit) {
+ this._params!.submit(true);
+ }
+ this.closeDialog();
+ }
+
+ private _cancel(): void {
+ this._params?.cancel?.();
+ this.closeDialog();
+ }
+
+ public closeDialog(): void {
+ this._params = undefined;
+ fireEvent(this, "dialog-closed", { dialog: this.localName });
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ p {
+ margin: 0;
+ color: var(--primary-text-color);
+ }
+ ha-dialog {
+ /* Place above other dialogs */
+ --dialog-z-index: 104;
+ }
+ @media all and (min-width: 600px) {
+ ha-dialog {
+ --mdc-dialog-min-width: 400px;
+ }
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "dialog-update-backup": DialogBox;
+ }
+}
diff --git a/src/dialogs/update_backup/show-update-backup-dialog.ts b/src/dialogs/update_backup/show-update-backup-dialog.ts
new file mode 100644
index 000000000000..fcb0aa7a4156
--- /dev/null
+++ b/src/dialogs/update_backup/show-update-backup-dialog.ts
@@ -0,0 +1,35 @@
+import { fireEvent } from "../../common/dom/fire_event";
+
+export interface UpdateBackupDialogParams {
+ submit?: (response: boolean) => void;
+ cancel?: () => void;
+}
+
+export const showUpdateBackupDialogParams = (
+ element: HTMLElement,
+ dialogParams: UpdateBackupDialogParams
+) =>
+ new Promise((resolve) => {
+ const origCancel = dialogParams.cancel;
+ const origSubmit = dialogParams.submit;
+
+ fireEvent(element, "show-dialog", {
+ dialogTag: "dialog-update-backup",
+ dialogImport: () => import("./dialog-update-backup"),
+ dialogParams: {
+ ...dialogParams,
+ cancel: () => {
+ resolve(null);
+ if (origCancel) {
+ origCancel();
+ }
+ },
+ submit: (response: boolean) => {
+ resolve(response);
+ if (origSubmit) {
+ origSubmit(response);
+ }
+ },
+ },
+ });
+ });
diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts
index 8f164875b72c..700c6e8ea977 100644
--- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts
+++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts
@@ -668,7 +668,12 @@ export class HaVoiceCommandDialog extends LitElement {
ha-button-menu {
--mdc-theme-on-primary: var(--text-primary-color);
--mdc-theme-primary: var(--primary-color);
- margin: -8px 0 0 -8px;
+ margin-top: -8px;
+ margin-bottom: 0;
+ margin-right: 0;
+ margin-inline-end: 0;
+ margin-left: -8px;
+ margin-inline-start: -8px;
}
ha-button-menu ha-button {
--mdc-theme-primary: var(--secondary-text-color);
@@ -689,7 +694,7 @@ export class HaVoiceCommandDialog extends LitElement {
height: 28px;
margin-left: 4px;
margin-inline-start: 4px;
- margin-inline-end: 4px;
+ margin-inline-end: initial;
direction: var(--direction);
}
ha-list-item {
@@ -698,7 +703,7 @@ export class HaVoiceCommandDialog extends LitElement {
ha-list-item ha-svg-icon {
margin-left: 4px;
margin-inline-start: 4px;
- margin-inline-end: 4px;
+ margin-inline-end: initial;
direction: var(--direction);
display: block;
}
diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts
index f891f6b1f9b3..7d3e90d623f1 100644
--- a/src/layouts/hass-tabs-subpage-data-table.ts
+++ b/src/layouts/hass-tabs-subpage-data-table.ts
@@ -124,6 +124,12 @@ export class HaTabsSubpageDataTable extends LitElement {
*/
@property({ type: String }) public noDataText?: string;
+ /**
+ * Hides the data table and show an empty message.
+ * @type {Boolean}
+ */
+ @property({ type: Boolean }) public empty = false;
+
@property() public route!: Route;
/**
@@ -198,56 +204,61 @@ export class HaTabsSubpageDataTable extends LitElement {
.mainPage=${this.mainPage}
.supervisor=${this.supervisor}
>
- ${!this.hideFilterMenu
- ? html`
-
- ${this.narrow
+ ${this.empty
+ ? html`
+ ${this.noDataText}
+
`
+ : html`${!this.hideFilterMenu
+ ? html`
+
+ ${this.narrow
+ ? html`
+
+ `
+ : ""}
+
+ `
+ : ""}
+ ${this.narrow
+ ? html`
+
+ `
+ : ""}
+
+ ${!this.narrow
? html`
-
- `
- : ""}
- ${this.narrow
- ? html`
-
- `
- : ""}
-
- ${!this.narrow
- ? html`
-
-
-
-
-
- `
- : html`
`}
-
+ : html`
`}
+ `}
+
`;
@@ -374,6 +385,16 @@ export class HaTabsSubpageDataTable extends LitElement {
.filter-menu {
position: relative;
}
+ .center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ box-sizing: border-box;
+ height: 100%;
+ width: 100%;
+ padding: 16px;
+ }
`;
}
}
diff --git a/src/panels/calendar/dialog-calendar-event-detail.ts b/src/panels/calendar/dialog-calendar-event-detail.ts
index 6172e79b4ca8..ee091eac1652 100644
--- a/src/panels/calendar/dialog-calendar-event-detail.ts
+++ b/src/panels/calendar/dialog-calendar-event-detail.ts
@@ -1,8 +1,8 @@
import "@material/mwc-button";
-import { mdiCalendarClock, mdiClose } from "@mdi/js";
+import { mdiCalendarClock } from "@mdi/js";
import { toDate } from "date-fns-tz";
import { addDays, isSameDay } from "date-fns/esm";
-import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
+import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators";
import { formatDate } from "../../common/datetime/format_date";
import { formatDateTime } from "../../common/datetime/format_date_time";
@@ -11,6 +11,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { isDate } from "../../common/string/is_date";
import "../../components/entity/state-info";
import "../../components/ha-date-input";
+import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-time-input";
import {
CalendarEventMutableParams,
@@ -65,15 +66,7 @@ class DialogCalendarEventDetail extends LitElement {
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
- .heading=${html`
-
-
- `}
+ .heading=${createCloseHeading(this.hass, this._data!.summary)}
>
${this._error
diff --git a/src/panels/calendar/dialog-calendar-event-editor.ts b/src/panels/calendar/dialog-calendar-event-editor.ts
index 2cd0bb0f5d36..e5925b1243e2 100644
--- a/src/panels/calendar/dialog-calendar-event-editor.ts
+++ b/src/panels/calendar/dialog-calendar-event-editor.ts
@@ -1,5 +1,4 @@
import "@material/mwc-button";
-import { mdiClose } from "@mdi/js";
import { formatInTimeZone, toDate } from "date-fns-tz";
import {
addDays,
@@ -9,7 +8,7 @@ import {
startOfHour,
} from "date-fns/esm";
import { HassEntity } from "home-assistant-js-websocket";
-import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
+import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
@@ -18,23 +17,24 @@ import { supportsFeature } from "../../common/entity/supports-feature";
import { isDate } from "../../common/string/is_date";
import "../../components/entity/ha-entity-picker";
import "../../components/ha-date-input";
+import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-textarea";
import "../../components/ha-time-input";
import {
CalendarEntityFeature,
CalendarEventMutableParams,
+ RecurrenceRange,
createCalendarEvent,
deleteCalendarEvent,
- RecurrenceRange,
updateCalendarEvent,
} from "../../data/calendar";
+import { TimeZone } from "../../data/translation";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import "../lovelace/components/hui-generic-entity-row";
import "./ha-recurrence-rule-editor";
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
import { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor";
-import { TimeZone } from "../../data/translation";
const CALENDAR_DOMAINS = ["calendar"];
@@ -142,19 +142,12 @@ class DialogCalendarEventEditor extends LitElement {
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
- .heading=${html`
-
-
- `}
+ .heading=${createCloseHeading(
+ this.hass,
+ this.hass.localize(
+ `ui.components.calendar.event.${isCreate ? "add" : "edit"}`
+ )
+ )}
>
${this._error
@@ -584,9 +577,11 @@ class DialogCalendarEventEditor extends LitElement {
return [
haStyleDialog,
css`
- ha-dialog {
- --mdc-dialog-min-width: min(600px, 95vw);
- --mdc-dialog-max-width: min(600px, 95vw);
+ @media all and (min-width: 450px and min-height: 500px) {
+ ha-dialog {
+ --mdc-dialog-min-width: min(600px, 95vw);
+ --mdc-dialog-max-width: min(600px, 95vw);
+ }
}
state-info {
line-height: 40px;
diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts
index 2c114cd946a3..0e58c0e8b2b5 100644
--- a/src/panels/config/automation/action/ha-automation-action-row.ts
+++ b/src/panels/config/automation/action/ha-automation-action-row.ts
@@ -29,6 +29,8 @@ import { classMap } from "lit/directives/class-map";
import { storage } from "../../../../common/decorators/storage";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
+import { computeDomain } from "../../../../common/entity/compute_domain";
+import { domainIconWithoutDefault } from "../../../../common/entity/domain_icon";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-alert";
@@ -37,7 +39,7 @@ import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
-import { ACTION_TYPES, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
+import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
@@ -82,9 +84,9 @@ export const getType = (action: Action | undefined) => {
if (["and", "or", "not"].some((key) => key in action)) {
return "condition" as const;
}
- return Object.keys(ACTION_TYPES).find(
+ return Object.keys(ACTION_ICONS).find(
(option) => option in action
- ) as keyof typeof ACTION_TYPES;
+ ) as keyof typeof ACTION_ICONS;
};
export interface ActionElement extends LitElement {
@@ -190,7 +192,13 @@ export default class HaAutomationActionRow extends LitElement {
${capitalizeFirstLetter(
describeAction(this.hass, this._entityReg, this.action)
diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts
index 5c85b509c137..ce312a7bd399 100644
--- a/src/panels/config/automation/action/ha-automation-action.ts
+++ b/src/panels/config/automation/action/ha-automation-action.ts
@@ -1,57 +1,26 @@
import "@material/mwc-button";
-import type { ActionDetail } from "@material/mwc-list";
-import {
- mdiArrowDown,
- mdiArrowUp,
- mdiContentPaste,
- mdiDrag,
- mdiPlus,
-} from "@mdi/js";
+import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
-import {
- CSSResultGroup,
- LitElement,
- PropertyValues,
- css,
- html,
- nothing,
-} from "lit";
+import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
-import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
-import { stringCompare } from "../../../../common/string/compare";
-import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button";
-import "../../../../components/ha-button-menu";
-import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
-import { ACTION_TYPES } from "../../../../data/action";
-import { AutomationClipboard } from "../../../../data/automation";
+import { getService, isService } from "../../../../data/action";
+import type { AutomationClipboard } from "../../../../data/automation";
import { Action } from "../../../../data/script";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
-import { Entries, HomeAssistant } from "../../../../types";
+import { HomeAssistant } from "../../../../types";
+import {
+ PASTE_VALUE,
+ showAddAutomationElementDialog,
+} from "../show-add-automation-element-dialog";
import type HaAutomationActionRow from "./ha-automation-action-row";
import { getType } from "./ha-automation-action-row";
-import "./types/ha-automation-action-activate_scene";
-import "./types/ha-automation-action-choose";
-import "./types/ha-automation-action-condition";
-import "./types/ha-automation-action-delay";
-import "./types/ha-automation-action-device_id";
-import "./types/ha-automation-action-event";
-import "./types/ha-automation-action-if";
-import "./types/ha-automation-action-parallel";
-import "./types/ha-automation-action-play_media";
-import "./types/ha-automation-action-repeat";
-import "./types/ha-automation-action-service";
-import "./types/ha-automation-action-stop";
-import "./types/ha-automation-action-wait_for_trigger";
-import "./types/ha-automation-action-wait_template";
-
-const PASTE_VALUE = "__paste__";
@customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement {
@@ -150,42 +119,27 @@ export default class HaAutomationAction extends LitElement {
`
)}
-
+
- ${this._clipboard?.action
- ? html`
- ${this.hass.localize(
- "ui.panel.config.automation.editor.actions.paste"
- )}
- (${this.hass.localize(
- `ui.panel.config.automation.editor.actions.type.${
- getType(this._clipboard.action) || "unknown"
- }.label`
- )})
- `
- : nothing}
- ${this._processedTypes(this.hass.localize).map(
- ([opt, label, icon]) => html`
-
- ${label}
- `
- )}
-
+
+
+
+
`;
}
@@ -213,6 +167,44 @@ export default class HaAutomationAction extends LitElement {
}
}
+ private _addActionDialog() {
+ showAddAutomationElementDialog(this, {
+ type: "action",
+ add: this._addAction,
+ clipboardItem: getType(this._clipboard?.action),
+ });
+ }
+
+ private _addActionBuildingBlockDialog() {
+ showAddAutomationElementDialog(this, {
+ type: "action",
+ add: this._addAction,
+ clipboardItem: getType(this._clipboard?.action),
+ group: "building_blocks",
+ });
+ }
+
+ private _addAction = (action: string) => {
+ let actions: Action[];
+ if (action === PASTE_VALUE) {
+ actions = this.actions.concat(deepClone(this._clipboard!.action));
+ } else if (isService(action)) {
+ actions = this.actions.concat({
+ service: getService(action),
+ metadata: {},
+ });
+ } else {
+ const elClass = customElements.get(
+ `ha-automation-action-${action}`
+ ) as CustomElementConstructor & { defaultConfig: Action };
+ actions = this.actions.concat(
+ elClass ? { ...elClass.defaultConfig } : { [action]: {} }
+ );
+ }
+ this._focusLastActionOnChange = true;
+ fireEvent(this, "value-changed", { value: actions });
+ };
+
private async _enterReOrderMode(ev: CustomEvent) {
if (this.nested) return;
ev.stopPropagation();
@@ -258,25 +250,6 @@ export default class HaAutomationAction extends LitElement {
return this._actionKeys.get(action)!;
}
- private _addAction(ev: CustomEvent) {
- const action = (ev.currentTarget as HaSelect).items[ev.detail.index].value;
-
- let actions: Action[];
- if (action === PASTE_VALUE) {
- actions = this.actions.concat(deepClone(this._clipboard!.action));
- } else {
- const elClass = customElements.get(
- `ha-automation-action-${action}`
- ) as CustomElementConstructor & { defaultConfig: Action };
-
- actions = this.actions.concat(
- elClass ? { ...elClass.defaultConfig } : { [action]: {} }
- );
- }
- this._focusLastActionOnChange = true;
- fireEvent(this, "value-changed", { value: actions });
- }
-
private _moveUp(ev) {
const index = (ev.target as any).index;
const newIndex = index - 1;
@@ -328,22 +301,6 @@ export default class HaAutomationAction extends LitElement {
});
}
- private _processedTypes = memoizeOne(
- (localize: LocalizeFunc): [string, string, string][] =>
- (Object.entries(ACTION_TYPES) as Entries)
- .map(
- ([action, icon]) =>
- [
- action,
- localize(
- `ui.panel.config.automation.editor.actions.type.${action}.label`
- ),
- icon,
- ] as [string, string, string]
- )
- .sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
- );
-
static get styles(): CSSResultGroup {
return [
sortableStyles,
@@ -363,13 +320,19 @@ export default class HaAutomationAction extends LitElement {
overflow: hidden;
}
.handle {
- cursor: move;
+ cursor: move; /* fallback if grab cursor is unsupported */
+ cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
+ .buttons {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
`,
];
}
diff --git a/src/panels/config/automation/action/types/ha-automation-action-choose.ts b/src/panels/config/automation/action/types/ha-automation-action-choose.ts
index 9051fbb75cd0..5c0ee0763790 100644
--- a/src/panels/config/automation/action/types/ha-automation-action-choose.ts
+++ b/src/panels/config/automation/action/types/ha-automation-action-choose.ts
@@ -543,7 +543,8 @@ export class HaChooseAction extends LitElement implements ActionElement {
padding: 0 16px 16px 16px;
}
.handle {
- cursor: move;
+ cursor: move; /* fallback if grab cursor is unsupported */
+ cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {
diff --git a/src/panels/config/automation/action/types/ha-automation-action-condition.ts b/src/panels/config/automation/action/types/ha-automation-action-condition.ts
index c72da97aac7e..d62176812d8b 100644
--- a/src/panels/config/automation/action/types/ha-automation-action-condition.ts
+++ b/src/panels/config/automation/action/types/ha-automation-action-condition.ts
@@ -7,7 +7,7 @@ import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select";
import type { Condition } from "../../../../../data/automation";
-import { CONDITION_TYPES } from "../../../../../data/condition";
+import { CONDITION_ICONS } from "../../../../../data/condition";
import { Entries, HomeAssistant } from "../../../../../types";
import "../../condition/ha-automation-condition-editor";
import type { ActionElement } from "../ha-automation-action-row";
@@ -55,7 +55,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
- (Object.entries(CONDITION_TYPES) as Entries)
+ (Object.entries(CONDITION_ICONS) as Entries)
.map(
([condition, icon]) =>
[
diff --git a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts
index 890133d25019..7a1ad36e8156 100644
--- a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts
+++ b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts
@@ -9,6 +9,7 @@ import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row";
+import { isTemplate } from "../../../../../common/string/has-template";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
@@ -32,7 +33,12 @@ export class HaRepeatAction extends LitElement implements ActionElement {
}
private _schema = memoizeOne(
- (localize: LocalizeFunc, type: string, reOrderMode: boolean) =>
+ (
+ localize: LocalizeFunc,
+ type: string,
+ reOrderMode: boolean,
+ template: boolean
+ ) =>
[
{
name: "type",
@@ -53,7 +59,9 @@ export class HaRepeatAction extends LitElement implements ActionElement {
{
name: "count",
required: true,
- selector: { number: { mode: "box", min: 1 } },
+ selector: template
+ ? ({ template: {} } as const)
+ : ({ number: { mode: "box", min: 1 } } as const),
},
] as const)
: []),
@@ -89,10 +97,13 @@ export class HaRepeatAction extends LitElement implements ActionElement {
const schema = this._schema(
this.hass.localize,
type ?? "count",
- this.reOrderMode
+ this.reOrderMode,
+ "count" in action && typeof action.count === "string"
+ ? isTemplate(action.count)
+ : false
);
const data = { ...action, type };
- return html` ;
+
+ @query("ha-dialog") private _dialog?: HaDialog;
+
+ private _fullScreen = false;
+
+ @state() private _width?: number;
+
+ @state() private _height?: number;
+
+ public showDialog(params): void {
+ this._params = params;
+ this._group = params.group;
+ if (this._params?.type === "action") {
+ this.hass.loadBackendTranslation("services");
+ this._fetchManifests();
+ }
+ this._fullScreen = matchMedia(
+ "all and (max-width: 450px), all and (max-height: 500px)"
+ ).matches;
+ }
+
+ public closeDialog(): void {
+ if (this._params) {
+ fireEvent(this, "dialog-closed", { dialog: this.localName });
+ }
+ this._height = undefined;
+ this._width = undefined;
+ this._params = undefined;
+ this._group = undefined;
+ this._prev = undefined;
+ this._filter = "";
+ this._manifests = undefined;
+ this._domains = undefined;
+ }
+
+ private _convertToItem = (
+ key: string,
+ options,
+ type: AddAutomationElementDialogParams["type"],
+ localize: LocalizeFunc
+ ): ListItem => ({
+ group: Boolean(options.members),
+ key,
+ name: localize(
+ // @ts-ignore
+ `ui.panel.config.automation.editor.${type}s.${
+ options.members ? "groups" : "type"
+ }.${key}.label`
+ ),
+ description: localize(
+ // @ts-ignore
+ `ui.panel.config.automation.editor.${type}s.${
+ options.members ? "groups" : "type"
+ }.${key}.description${options.members ? "" : ".picker"}`
+ ),
+ icon: options.icon || TYPES[type].icons[key],
+ });
+
+ private _getFilteredItems = memoizeOne(
+ (
+ type: AddAutomationElementDialogParams["type"],
+ root: AddAutomationElementDialogParams["root"],
+ group: string | undefined,
+ filter: string,
+ localize: LocalizeFunc,
+ services: HomeAssistant["services"],
+ manifests?: DomainManifestLookup
+ ): ListItem[] => {
+ const groups: AutomationElementGroup = group
+ ? isService(group)
+ ? {}
+ : TYPES[type].groups[group].members!
+ : TYPES[type].groups;
+
+ if (type === "condition" && group === "other" && !root) {
+ groups.trigger = {};
+ }
+
+ const flattenGroups = (grp: AutomationElementGroup) =>
+ Object.entries(grp).map(([key, options]) =>
+ options.members
+ ? flattenGroups(options.members)
+ : this._convertToItem(key, options, type, localize)
+ );
+
+ const items = flattenGroups(groups).flat();
+
+ if (type === "action") {
+ items.push(...this._services(localize, services, manifests, group));
+ }
+
+ const options: IFuseOptions = {
+ keys: ["key", "name", "description"],
+ isCaseSensitive: false,
+ minMatchCharLength: Math.min(filter.length, 2),
+ threshold: 0.2,
+ };
+ const fuse = new Fuse(items, options);
+ return fuse.search(filter).map((result) => result.item);
+ }
+ );
+
+ private _getGroupItems = memoizeOne(
+ (
+ type: AddAutomationElementDialogParams["type"],
+ root: AddAutomationElementDialogParams["root"],
+ group: string | undefined,
+ domains: Set | undefined,
+ localize: LocalizeFunc,
+ services: HomeAssistant["services"],
+ manifests?: DomainManifestLookup
+ ): ListItem[] => {
+ if (type === "action" && isService(group)) {
+ const result = this._services(localize, services, manifests, group);
+ if (group === `${SERVICE_PREFIX}media_player`) {
+ result.unshift(this._convertToItem("play_media", {}, type, localize));
+ }
+ return result;
+ }
+
+ const groups: AutomationElementGroup = group
+ ? TYPES[type].groups[group].members!
+ : TYPES[type].groups;
+
+ if (type === "condition" && group === "other" && !root) {
+ groups.trigger = {};
+ }
+
+ const result = Object.entries(groups).map(([key, options]) =>
+ this._convertToItem(key, options, type, localize)
+ );
+
+ if (type === "action") {
+ if (!this._group) {
+ result.unshift(
+ ...this._serviceGroups(
+ localize,
+ services,
+ manifests,
+ domains,
+ undefined
+ )
+ );
+ } else if (this._group === "helpers") {
+ result.unshift(
+ ...this._serviceGroups(
+ localize,
+ services,
+ manifests,
+ domains,
+ "helper"
+ )
+ );
+ } else if (this._group === "other") {
+ result.unshift(
+ ...this._serviceGroups(
+ localize,
+ services,
+ manifests,
+ domains,
+ "other"
+ )
+ );
+ }
+ }
+
+ return result.sort((a, b) => {
+ if (a.group && b.group) {
+ return 0;
+ }
+ if (a.group && !b.group) {
+ return 1;
+ }
+ if (!a.group && b.group) {
+ return -1;
+ }
+ return stringCompare(a.name, b.name, this.hass.locale.language);
+ });
+ }
+ );
+
+ private _serviceGroups = (
+ localize: LocalizeFunc,
+ services: HomeAssistant["services"],
+ manifests: DomainManifestLookup | undefined,
+ domains: Set | undefined,
+ type: "helper" | "other" | undefined
+ ): ListItem[] => {
+ if (!services || !manifests) {
+ return [];
+ }
+ const result: ListItem[] = [];
+ Object.keys(services).forEach((domain) => {
+ const manifest = manifests[domain];
+ const domainUsed = !domains ? true : domains.has(domain);
+ if (
+ (type === undefined &&
+ (ENTITY_DOMAINS_MAIN.has(domain) ||
+ (manifest?.integration_type === "entity" &&
+ domainUsed &&
+ !ENTITY_DOMAINS_OTHER.has(domain)))) ||
+ (type === "helper" && manifest?.integration_type === "helper") ||
+ (type === "other" &&
+ !ENTITY_DOMAINS_MAIN.has(domain) &&
+ (ENTITY_DOMAINS_OTHER.has(domain) ||
+ (!domainUsed && manifest?.integration_type === "entity") ||
+ !["helper", "entity"].includes(manifest?.integration_type || "")))
+ ) {
+ const icon = domainIconWithoutDefault(domain);
+ result.push({
+ group: true,
+ icon,
+ image: !icon
+ ? brandsUrl({
+ domain,
+ type: "icon",
+ darkOptimized: this.hass.themes?.darkMode,
+ })
+ : undefined,
+ key: `${SERVICE_PREFIX}${domain}`,
+ name: domainToName(localize, domain, manifest),
+ description: "",
+ });
+ }
+ });
+ return result.sort((a, b) =>
+ stringCompare(a.name, b.name, this.hass.locale.language)
+ );
+ };
+
+ private _services = memoizeOne(
+ (
+ localize: LocalizeFunc,
+ services: HomeAssistant["services"],
+ manifests: DomainManifestLookup | undefined,
+ group?: string
+ ): ListItem[] => {
+ if (!services) {
+ return [];
+ }
+ const result: ListItem[] = [];
+
+ let domain: string | undefined;
+
+ if (isService(group)) {
+ domain = getService(group!);
+ }
+
+ const addDomain = (dmn: string) => {
+ const services_keys = Object.keys(services[dmn]);
+
+ for (const service of services_keys) {
+ const icon = domainIconWithoutDefault(dmn);
+ result.push({
+ group: false,
+ icon,
+ image: !icon
+ ? brandsUrl({
+ domain: dmn,
+ type: "icon",
+ darkOptimized: this.hass.themes?.darkMode,
+ })
+ : undefined,
+ key: `${SERVICE_PREFIX}${dmn}.${service}`,
+ name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
+ this.hass.localize(`component.${dmn}.services.${service}.name`) ||
+ services[dmn][service]?.name ||
+ service
+ }`,
+ description:
+ this.hass.localize(
+ `component.${domain}.services.${service}.description`
+ ) || services[dmn][service]?.description,
+ });
+ }
+ };
+
+ if (domain) {
+ addDomain(domain);
+ return result.sort((a, b) =>
+ stringCompare(a.name, b.name, this.hass.locale.language)
+ );
+ }
+
+ if (group && !["helpers", "other"].includes(group)) {
+ return [];
+ }
+
+ Object.keys(services)
+ .sort()
+ .forEach((dmn) => {
+ const manifest = manifests?.[dmn];
+ if (group === "helpers" && manifest?.integration_type !== "helper") {
+ return;
+ }
+ if (
+ group === "other" &&
+ (ENTITY_DOMAINS_OTHER.has(dmn) ||
+ ["helper", "entity"].includes(manifest?.integration_type || ""))
+ ) {
+ return;
+ }
+ addDomain(dmn);
+ });
+
+ return result;
+ }
+ );
+
+ private async _fetchManifests() {
+ const manifests = {};
+ const fetched = await fetchIntegrationManifests(this.hass);
+ for (const manifest of fetched) {
+ manifests[manifest.domain] = manifest;
+ }
+ this._manifests = manifests;
+ }
+
+ protected _opened(): void {
+ // Store the width and height so that when we search, box doesn't jump
+ const boundingRect =
+ this.shadowRoot!.querySelector("mwc-list")?.getBoundingClientRect();
+ this._width = boundingRect?.width;
+ this._height = boundingRect?.height;
+ }
+
+ protected willUpdate(changedProperties: PropertyValues): void {
+ if (
+ this._params?.type === "action" &&
+ changedProperties.has("hass") &&
+ changedProperties.get("hass")?.states !== this.hass.states
+ ) {
+ const domains = new Set(Object.keys(this.hass.states).map(computeDomain));
+ if (!deepEqual(domains, this._domains)) {
+ this._domains = domains;
+ }
+ }
+ }
+
+ protected render() {
+ if (!this._params) {
+ return nothing;
+ }
+
+ const items = this._filter
+ ? this._getFilteredItems(
+ this._params.type,
+ this._params.root,
+ this._group,
+ this._filter,
+ this.hass.localize,
+ this.hass.services,
+ this._manifests
+ )
+ : this._getGroupItems(
+ this._params.type,
+ this._params.root,
+ this._group,
+ this._domains,
+ this.hass.localize,
+ this.hass.services,
+ this._manifests
+ );
+
+ const groupName = isService(this._group)
+ ? domainToName(
+ this.hass.localize,
+ getService(this._group!),
+ this._manifests?.[getService(this._group!)]
+ )
+ : this.hass.localize(
+ // @ts-ignore
+ `ui.panel.config.automation.editor.${this._params.type}s.groups.${this._group}.label`
+ );
+
+ return html`
+
+
+
+ ${this._group
+ ? groupName
+ : this.hass.localize(
+ `ui.panel.config.automation.editor.${this._params.type}s.add`
+ )}
+ ${this._group && this._group !== this._params.group
+ ? html` `
+ : html` `}
+
+
+
+
+ ${this._params.clipboardItem &&
+ !this._filter &&
+ (!this._group ||
+ items.find((item) => item.key === this._params!.clipboardItem))
+ ? html`
+ ${this.hass.localize(
+ `ui.panel.config.automation.editor.${this._params.type}s.paste`
+ )}
+ ${this.hass.localize(
+ // @ts-ignore
+ `ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
+ )}
+
+
+ `
+ : ""}
+ ${repeat(
+ items,
+ (item) => item.key,
+ (item) => html`
+
+ ${item.name}
+ ${item.description}
+ ${item.icon
+ ? html` `
+ : html` `}
+ ${item.group
+ ? html` `
+ : html` `}
+
+ `
+ )}
+
+
+ `;
+ }
+
+ private _back() {
+ this._dialog!.scrollToPos(0, 0);
+ if (this._filter) {
+ this._filter = "";
+ return;
+ }
+ if (this._prev) {
+ this._group = this._prev;
+ this._prev = undefined;
+ return;
+ }
+ this._group = undefined;
+ }
+
+ private _selected(ev) {
+ if (!shouldHandleRequestSelectedEvent(ev)) {
+ return;
+ }
+ this._dialog!.scrollToPos(0, 0);
+ const item = ev.currentTarget;
+ if (item.group) {
+ this._prev = this._group;
+ this._group = item.value;
+ return;
+ }
+ this._params!.add(item.value);
+ this.closeDialog();
+ }
+
+ private _filterChanged(ev) {
+ this._filter = ev.detail.value;
+ }
+
+ static get styles(): CSSResultGroup {
+ return [
+ haStyle,
+ haStyleDialog,
+ css`
+ ha-dialog {
+ --dialog-content-padding: 0;
+ --mdc-dialog-max-height: 60vh;
+ }
+ @media all and (min-width: 550px) {
+ ha-dialog {
+ --mdc-dialog-min-width: 500px;
+ }
+ }
+ ha-icon-next {
+ width: 24px;
+ }
+ mwc-list {
+ max-height: 670px;
+ max-width: 100vw;
+ }
+ search-input {
+ display: block;
+ margin: 0 16px;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "add-automation-element-dialog": DialogAddAutomationElement;
+ }
+}
diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts
index 7e8bb4e270a3..1b51a9d278c1 100644
--- a/src/panels/config/automation/condition/ha-automation-condition-row.ts
+++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts
@@ -29,7 +29,7 @@ import "../../../../components/ha-icon-button";
import type { AutomationClipboard } from "../../../../data/automation";
import { Condition, testCondition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n";
-import { CONDITION_TYPES } from "../../../../data/condition";
+import { CONDITION_ICONS } from "../../../../data/condition";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
@@ -123,7 +123,7 @@ export default class HaAutomationConditionRow extends LitElement {
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
diff --git a/src/panels/config/automation/condition/ha-automation-condition.ts b/src/panels/config/automation/condition/ha-automation-condition.ts
index f562d1220354..ecf4f8823770 100644
--- a/src/panels/config/automation/condition/ha-automation-condition.ts
+++ b/src/panels/config/automation/condition/ha-automation-condition.ts
@@ -1,25 +1,18 @@
import "@material/mwc-button";
-import type { ActionDetail } from "@material/mwc-list";
-import {
- mdiArrowDown,
- mdiArrowUp,
- mdiContentPaste,
- mdiDrag,
- mdiPlus,
-} from "@mdi/js";
+import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
- css,
CSSResultGroup,
- html,
LitElement,
- nothing,
PropertyValues,
+ css,
+ html,
+ nothing,
} from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
-import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs";
+import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
@@ -28,30 +21,15 @@ import type {
AutomationClipboard,
Condition,
} from "../../../../data/automation";
-import type { Entries, HomeAssistant } from "../../../../types";
-import "./ha-automation-condition-row";
-import type HaAutomationConditionRow from "./ha-automation-condition-row";
-// Uncommenting these and this element doesn't load
-// import "./types/ha-automation-condition-not";
-// import "./types/ha-automation-condition-or";
-import { storage } from "../../../../common/decorators/storage";
-import { stringCompare } from "../../../../common/string/compare";
-import type { LocalizeFunc } from "../../../../common/translations/localize";
-import type { HaSelect } from "../../../../components/ha-select";
-import { CONDITION_TYPES } from "../../../../data/condition";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
-import "./types/ha-automation-condition-and";
-import "./types/ha-automation-condition-device";
-import "./types/ha-automation-condition-numeric_state";
-import "./types/ha-automation-condition-state";
-import "./types/ha-automation-condition-sun";
-import "./types/ha-automation-condition-template";
-import "./types/ha-automation-condition-time";
-import "./types/ha-automation-condition-trigger";
-import "./types/ha-automation-condition-zone";
-
-const PASTE_VALUE = "__paste__";
+import type { HomeAssistant } from "../../../../types";
+import {
+ PASTE_VALUE,
+ showAddAutomationElementDialog,
+} from "../show-add-automation-element-dialog";
+import "./ha-automation-condition-row";
+import type HaAutomationConditionRow from "./ha-automation-condition-row";
@customElement("ha-automation-condition")
export default class HaAutomationCondition extends LitElement {
@@ -197,43 +175,70 @@ export default class HaAutomationCondition extends LitElement {
`
)}
-
+
- ${this._clipboard?.condition
- ? html`
- ${this.hass.localize(
- "ui.panel.config.automation.editor.conditions.paste"
- )}
- (${this.hass.localize(
- `ui.panel.config.automation.editor.conditions.type.${this._clipboard.condition.condition}.label`
- )})
- `
- : nothing}
- ${this._processedTypes(this.hass.localize).map(
- ([opt, label, icon]) => html`
-
- ${label}
- `
- )}
-
+
+
+
+
`;
}
+ private _addConditionDialog() {
+ showAddAutomationElementDialog(this, {
+ type: "condition",
+ add: this._addCondition,
+ root: !this.nested,
+ clipboardItem: this._clipboard?.condition?.condition,
+ });
+ }
+
+ private _addConditionBuildingBlockDialog() {
+ showAddAutomationElementDialog(this, {
+ type: "condition",
+ add: this._addCondition,
+ clipboardItem: this._clipboard?.condition?.condition,
+ group: "building_blocks",
+ });
+ }
+
+ private _addCondition = (value) => {
+ let conditions: Condition[];
+ if (value === PASTE_VALUE) {
+ conditions = this.conditions.concat(
+ deepClone(this._clipboard!.condition)
+ );
+ } else {
+ const condition = value as Condition["condition"];
+ const elClass = customElements.get(
+ `ha-automation-condition-${condition}`
+ ) as CustomElementConstructor & {
+ defaultConfig: Omit;
+ };
+ conditions = this.conditions.concat({
+ condition: condition as any,
+ ...elClass.defaultConfig,
+ });
+ }
+ this._focusLastConditionOnChange = true;
+ fireEvent(this, "value-changed", { value: conditions });
+ };
+
private async _enterReOrderMode(ev: CustomEvent) {
if (this.nested) return;
ev.stopPropagation();
@@ -282,32 +287,6 @@ export default class HaAutomationCondition extends LitElement {
return this._conditionKeys.get(condition)!;
}
- private _addCondition(ev: CustomEvent) {
- const value = (ev.currentTarget as HaSelect).items[ev.detail.index].value;
-
- let conditions: Condition[];
- if (value === PASTE_VALUE) {
- conditions = this.conditions.concat(
- deepClone(this._clipboard!.condition)
- );
- } else {
- const condition = value as Condition["condition"];
-
- const elClass = customElements.get(
- `ha-automation-condition-${condition}`
- ) as CustomElementConstructor & {
- defaultConfig: Omit;
- };
-
- conditions = this.conditions.concat({
- condition: condition as any,
- ...elClass.defaultConfig,
- });
- }
- this._focusLastConditionOnChange = true;
- fireEvent(this, "value-changed", { value: conditions });
- }
-
private _moveUp(ev) {
const index = (ev.target as any).index;
const newIndex = index - 1;
@@ -361,22 +340,6 @@ export default class HaAutomationCondition extends LitElement {
});
}
- private _processedTypes = memoizeOne(
- (localize: LocalizeFunc): [string, string, string][] =>
- (Object.entries(CONDITION_TYPES) as Entries)
- .map(
- ([condition, icon]) =>
- [
- condition,
- localize(
- `ui.panel.config.automation.editor.conditions.type.${condition}.label`
- ),
- icon,
- ] as [string, string, string]
- )
- .sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
- );
-
static get styles(): CSSResultGroup {
return [
sortableStyles,
@@ -396,13 +359,19 @@ export default class HaAutomationCondition extends LitElement {
overflow: hidden;
}
.handle {
- cursor: move;
+ cursor: move; /* fallback if grab cursor is unsupported */
+ cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
+ .buttons {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
`,
];
}
diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-device.ts b/src/panels/config/automation/condition/types/ha-automation-condition-device.ts
index 71568fe02c10..4593a5f70123 100644
--- a/src/panels/config/automation/condition/types/ha-automation-condition-device.ts
+++ b/src/panels/config/automation/condition/types/ha-automation-condition-device.ts
@@ -168,6 +168,7 @@ export class HaDeviceCondition extends LitElement {
}
ha-form {
+ display: block;
margin-top: 24px;
}
`;
diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts
index 2c6429d642e1..c8ca54c3d7ee 100644
--- a/src/panels/config/automation/ha-automation-editor.ts
+++ b/src/panels/config/automation/ha-automation-editor.ts
@@ -486,7 +486,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
value.valid
? ""
: html`${this.hass.localize(
- `ui.panel.config.automation.editor.${key}s.header`
+ `ui.panel.config.automation.editor.${key}s.name`
)}:
${value.error} `
);
diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts
index 2e0c91708691..7fe54ec1f818 100644
--- a/src/panels/config/automation/ha-automation-picker.ts
+++ b/src/panels/config/automation/ha-automation-picker.ts
@@ -7,11 +7,19 @@ import {
mdiPlay,
mdiPlayCircleOutline,
mdiPlus,
+ mdiRobotHappy,
mdiStopCircleOutline,
mdiTransitConnection,
} from "@mdi/js";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
-import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
+import {
+ css,
+ CSSResultGroup,
+ html,
+ LitElement,
+ nothing,
+ TemplateResult,
+} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { differenceInDays } from "date-fns/esm";
@@ -295,6 +303,7 @@ class HaAutomationPicker extends LitElement {
.activeFilters=${this._activeFilters}
.columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._automations(this.automations, this._filteredAutomations)}
+ .empty=${!this.automations.length}
@row-click=${this._handleRowClicked}
.noDataText=${this.hass.localize(
"ui.panel.config.automation.picker.no_automations"
@@ -318,6 +327,36 @@ class HaAutomationPicker extends LitElement {
@related-changed=${this._relatedFilterChanged}
>
+ ${!this.automations.length
+ ? html`
+
+
+ ${this.hass.localize(
+ "ui.panel.config.automation.picker.empty_header"
+ )}
+
+
+ ${this.hass.localize(
+ "ui.panel.config.automation.picker.empty_text_1"
+ )}
+
+
+ ${this.hass.localize(
+ "ui.panel.config.automation.picker.empty_text_2",
+ { user: this.hass.user?.name || "Alice" }
+ )}
+
+
+
+ ${this.hass.localize("ui.panel.config.common.learn_more")}
+
+
+
`
+ : nothing}
${this._traces === undefined
- ? html`Loading…
`
+ ? html`
+ ${this.hass!.localize("ui.common.loading")}
+
`
: this._traces.length === 0
- ? html`No traces found
`
+ ? html`
+ ${this.hass!.localize(
+ "ui.panel.config.automation.trace.no_traces_found"
+ )}
+
`
: this._trace === undefined
? ""
: html`
@@ -230,20 +240,17 @@ export class HaAutomationTrace extends LitElement {
- ${[
- ["details", "Step Details"],
- ["timeline", "Trace Timeline"],
- ["logbook", "Related logbook entries"],
- ["config", "Automation Config"],
- ].map(
- ([view, label]) => html`
+ ${TABS.map(
+ (view) => html`
- ${label}
+ ${this.hass!.localize(
+ `ui.panel.config.automation.trace.tabs.${view}`
+ )}
`
)}
@@ -257,7 +264,9 @@ export class HaAutomationTrace extends LitElement {
})}
@click=${this._showTab}
>
- Blueprint Config
+ ${this.hass!.localize(
+ `ui.panel.config.automation.trace.tabs.blueprint_config`
+ )}
`
: ""}
@@ -265,7 +274,7 @@ export class HaAutomationTrace extends LitElement {
${this._selected === undefined ||
this._logbookEntries === undefined ||
trackedNodes === undefined
- ? ""
+ ? nothing
: this._view === "details"
? html`
`
- : this._view === "config"
+ : this._view === "automation_config"
? html`
+ ${!ensureArray(this.config.trigger)?.length
+ ? html`
+ ${this.hass.localize(
+ "ui.panel.config.automation.editor.triggers.description"
+ )}
+
`
+ : nothing}
(${this.hass.localize("ui.common.optional")})
+ ${!ensureArray(this.config.condition)?.length
+ ? html`
+ ${this.hass.localize(
+ "ui.panel.config.automation.editor.conditions.description",
+ { user: this.hass.user?.name || "Alice" }
+ )}
+
`
+ : nothing}
+ ${!ensureArray(this.config.action)?.length
+ ? html`
+ ${this.hass.localize(
+ "ui.panel.config.automation.editor.actions.description"
+ )}
+
`
+ : nothing}
void;
+ clipboardItem: string | undefined;
+ root?: boolean;
+ group?: string;
+}
+const loadDialog = () => import("./add-automation-element-dialog");
+
+export const showAddAutomationElementDialog = (
+ element: HTMLElement,
+ dialogParams: AddAutomationElementDialogParams
+): void => {
+ fireEvent(element, "show-dialog", {
+ dialogTag: "add-automation-element-dialog",
+ dialogImport: loadDialog,
+ dialogParams,
+ });
+};
diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts
index 128f7288befd..5a4acb43c6c4 100644
--- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts
+++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts
@@ -37,7 +37,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
-import { TRIGGER_TYPES } from "../../../../data/trigger";
+import { TRIGGER_ICONS } from "../../../../data/trigger";
import {
showAlertDialog,
showConfirmationDialog,
@@ -150,7 +150,7 @@ export default class HaAutomationTriggerRow extends LitElement {
${describeTrigger(this.trigger, this.hass, this._entityReg)}
diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts
index 3be0e591619e..90467f61e0b8 100644
--- a/src/panels/config/automation/trigger/ha-automation-trigger.ts
+++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts
@@ -1,59 +1,25 @@
import "@material/mwc-button";
-import type { ActionDetail } from "@material/mwc-list";
-import {
- mdiArrowDown,
- mdiArrowUp,
- mdiContentPaste,
- mdiDrag,
- mdiPlus,
-} from "@mdi/js";
+import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
-import {
- CSSResultGroup,
- LitElement,
- PropertyValues,
- css,
- html,
- nothing,
-} from "lit";
+import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
-import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
-import { stringCompare } from "../../../../common/string/compare";
-import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
-import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import { AutomationClipboard, Trigger } from "../../../../data/automation";
-import { TRIGGER_TYPES } from "../../../../data/trigger";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
-import { Entries, HomeAssistant } from "../../../../types";
+import { HomeAssistant } from "../../../../types";
import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
-import "./types/ha-automation-trigger-calendar";
-import "./types/ha-automation-trigger-conversation";
-import "./types/ha-automation-trigger-device";
-import "./types/ha-automation-trigger-event";
-import "./types/ha-automation-trigger-geo_location";
-import "./types/ha-automation-trigger-homeassistant";
-import "./types/ha-automation-trigger-mqtt";
-import "./types/ha-automation-trigger-numeric_state";
-import "./types/ha-automation-trigger-persistent_notification";
-import "./types/ha-automation-trigger-state";
-import "./types/ha-automation-trigger-sun";
-import "./types/ha-automation-trigger-tag";
-import "./types/ha-automation-trigger-template";
-import "./types/ha-automation-trigger-time";
-import "./types/ha-automation-trigger-time_pattern";
-import "./types/ha-automation-trigger-webhook";
-import "./types/ha-automation-trigger-zone";
-
-const PASTE_VALUE = "__paste__";
+import {
+ PASTE_VALUE,
+ showAddAutomationElementDialog,
+} from "../show-add-automation-element-dialog";
@customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends LitElement {
@@ -147,47 +113,48 @@ export default class HaAutomationTrigger extends LitElement {
`
)}
-
-
-
-
- ${this._clipboard?.trigger
- ? html`
- ${this.hass.localize(
- "ui.panel.config.automation.editor.triggers.paste"
- )}
- (${this.hass.localize(
- `ui.panel.config.automation.editor.triggers.type.${this._clipboard.trigger.platform}.label`
- )})
- `
- : nothing}
- ${this._processedTypes(this.hass.localize).map(
- ([opt, label, icon]) => html`
-
- ${label}
- `
- )}
-
+
+
`;
}
+ private _addTriggerDialog() {
+ showAddAutomationElementDialog(this, {
+ type: "trigger",
+ add: this._addTrigger,
+ clipboardItem: this._clipboard?.trigger?.platform,
+ });
+ }
+
+ private _addTrigger = (value: string) => {
+ let triggers: Trigger[];
+ if (value === PASTE_VALUE) {
+ triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
+ } else {
+ const platform = value as Trigger["platform"];
+ const elClass = customElements.get(
+ `ha-automation-trigger-${platform}`
+ ) as CustomElementConstructor & {
+ defaultConfig: Omit;
+ };
+ triggers = this.triggers.concat({
+ platform: platform as any,
+ ...elClass.defaultConfig,
+ });
+ }
+ this._focusLastTriggerOnChange = true;
+ fireEvent(this, "value-changed", { value: triggers });
+ };
+
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
@@ -261,30 +228,6 @@ export default class HaAutomationTrigger extends LitElement {
return this._triggerKeys.get(action)!;
}
- private _addTrigger(ev: CustomEvent) {
- const value = (ev.currentTarget as HaSelect).items[ev.detail.index].value;
-
- let triggers: Trigger[];
- if (value === PASTE_VALUE) {
- triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
- } else {
- const platform = value as Trigger["platform"];
-
- const elClass = customElements.get(
- `ha-automation-trigger-${platform}`
- ) as CustomElementConstructor & {
- defaultConfig: Omit;
- };
-
- triggers = this.triggers.concat({
- platform: platform as any,
- ...elClass.defaultConfig,
- });
- }
- this._focusLastTriggerOnChange = true;
- fireEvent(this, "value-changed", { value: triggers });
- }
-
private _moveUp(ev) {
const index = (ev.target as any).index;
const newIndex = index - 1;
@@ -336,22 +279,6 @@ export default class HaAutomationTrigger extends LitElement {
});
}
- private _processedTypes = memoizeOne(
- (localize: LocalizeFunc): [string, string, string][] =>
- (Object.entries(TRIGGER_TYPES) as Entries)
- .map(
- ([action, icon]) =>
- [
- action,
- localize(
- `ui.panel.config.automation.editor.triggers.type.${action}.label`
- ),
- icon,
- ] as [string, string, string]
- )
- .sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
- );
-
static get styles(): CSSResultGroup {
return [
sortableStyles,
@@ -371,7 +298,8 @@ export default class HaAutomationTrigger extends LitElement {
overflow: hidden;
}
.handle {
- cursor: move;
+ cursor: move; /* fallback if grab cursor is unsupported */
+ cursor: grab;
padding: 12px;
}
.handle ha-svg-icon {
diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts
index 8b3a63575220..1b94ff6138b2 100644
--- a/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts
+++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-device.ts
@@ -174,6 +174,7 @@ export class HaDeviceTrigger extends LitElement {
}
ha-form {
+ display: block;
margin-top: 24px;
}
`;
diff --git a/src/panels/config/dashboard/ha-config-updates.ts b/src/panels/config/dashboard/ha-config-updates.ts
index 9b654a5b3185..e12e77609626 100644
--- a/src/panels/config/dashboard/ha-config-updates.ts
+++ b/src/panels/config/dashboard/ha-config-updates.ts
@@ -109,9 +109,11 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
${this.narrow && entity.attributes.in_progress
? html` `
: ""}
`
: html` `
: ""}
@@ -191,6 +196,8 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
}
ha-circular-progress.absolute {
position: absolute;
+ width: 40px;
+ height: 40px;
}
state-badge.updating {
opacity: 0.5;
diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts
index c586252c7d13..ca7c4fde9847 100644
--- a/src/panels/config/devices/ha-config-devices-dashboard.ts
+++ b/src/panels/config/devices/ha-config-devices-dashboard.ts
@@ -363,8 +363,8 @@ export class HaConfigDeviceDashboard extends LitElement {
sortable: true,
filterable: true,
type: "numeric",
- width: narrow ? "95px" : "15%",
- maxWidth: "95px",
+ width: narrow ? "105px" : "15%",
+ maxWidth: "105px",
valueColumn: "battery_level",
template: (device) => {
const batteryEntityPair = device.battery_entity;
diff --git a/src/panels/config/entities/entity-registry-settings-editor.ts b/src/panels/config/entities/entity-registry-settings-editor.ts
index 86e1405aa438..9e7213e41475 100644
--- a/src/panels/config/entities/entity-registry-settings-editor.ts
+++ b/src/panels/config/entities/entity-registry-settings-editor.ts
@@ -118,10 +118,12 @@ const OVERRIDE_DEVICE_CLASSES = {
"carbon_monoxide",
"moisture",
], // Alarm
+ ["connectivity"], // Connectivity
+ ["update"], // Update
],
};
-const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"];
+const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren", "valve"];
const PRECISIONS = [0, 1, 2, 3, 4, 5, 6];
diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts
index 76cb5e43c91f..f2e121e4ce08 100644
--- a/src/panels/config/entities/ha-config-entities.ts
+++ b/src/panels/config/entities/ha-config-entities.ts
@@ -74,7 +74,7 @@ export interface EntityRow extends StateEntity {
entity?: HassEntity;
unavailable: boolean;
restored: boolean;
- status: string;
+ status: string | undefined;
area?: string;
localized_platform: string;
}
@@ -429,7 +429,13 @@ export class HaConfigEntities extends LitElement {
? localize("ui.panel.config.entities.picker.status.unavailable")
: entry.disabled_by
? localize("ui.panel.config.entities.picker.status.disabled")
- : localize("ui.panel.config.entities.picker.status.ok"),
+ : entry.hidden_by
+ ? localize("ui.panel.config.entities.picker.status.hidden")
+ : entry.readonly
+ ? localize(
+ "ui.panel.config.entities.picker.status.readonly"
+ )
+ : undefined,
});
}
diff --git a/src/panels/config/helpers/forms/ha-input_select-form.ts b/src/panels/config/helpers/forms/ha-input_select-form.ts
index e65f951e4bb2..e8be0fe50418 100644
--- a/src/panels/config/helpers/forms/ha-input_select-form.ts
+++ b/src/panels/config/helpers/forms/ha-input_select-form.ts
@@ -285,7 +285,8 @@ class HaInputSelectForm extends LitElement {
margin-bottom: 8px;
}
.handle {
- cursor: move;
+ cursor: move; /* fallback if grab cursor is unsupported */
+ cursor: grab;
padding-right: 12px;
}
.handle ha-svg-icon {
diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts
index 14b1ed581c01..65edfbfccf67 100644
--- a/src/panels/config/integrations/dialog-add-integration.ts
+++ b/src/panels/config/integrations/dialog-add-integration.ts
@@ -3,21 +3,22 @@ import "@material/mwc-list/mwc-list";
import Fuse, { IFuseOptions } from "fuse.js";
import { HassConfig } from "home-assistant-js-websocket";
import {
- css,
- html,
LitElement,
PropertyValues,
TemplateResult,
+ css,
+ html,
nothing,
} from "lit";
import { customElement, state } from "lit/decorators";
+import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import {
- protocolIntegrationPicked,
PROTOCOL_INTEGRATIONS,
+ protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
@@ -34,10 +35,10 @@ import {
import {
Brand,
Brands,
- findIntegration,
- getIntegrationDescriptions,
Integration,
Integrations,
+ findIntegration,
+ getIntegrationDescriptions,
} from "../../../data/integrations";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import {
@@ -339,7 +340,9 @@ class AddIntegrationDialog extends LitElement {
!("integrations" in integration) &&
!this._flowsInProgress?.length
) {
- return "What type of device is it?";
+ return this.hass.localize(
+ "ui.panel.config.integrations.what_device_type"
+ );
}
if (
integration &&
@@ -347,9 +350,11 @@ class AddIntegrationDialog extends LitElement {
!("integrations" in integration) &&
this._flowsInProgress?.length
) {
- return "Want to add these discovered devices?";
+ return this.hass.localize(
+ "ui.panel.config.integrations.confirm_add_discovered"
+ );
}
- return "What do you want to add?";
+ return this.hass.localize("ui.panel.config.integrations.what_to_add");
}
private _renderIntegration(
@@ -424,8 +429,7 @@ class AddIntegrationDialog extends LitElement {
private _renderAll(integrations?: IntegrationListItem[]): TemplateResult {
return html`
${integrations
- ? html`
+ ? html`
${this._manifest?.integration_type
? this.hass.localize(
- `ui.panel.config.integrations.integration_page.entries_${this._manifest?.integration_type}`
+ `ui.panel.config.integrations.integration_page.entries_${this._manifest.integration_type}`
)
: this.hass.localize(
`ui.panel.config.integrations.integration_page.entries`
@@ -507,7 +507,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
${this._manifest?.integration_type
? this.hass.localize(
- `ui.panel.config.integrations.integration_page.add_${this._manifest?.integration_type}`
+ `ui.panel.config.integrations.integration_page.add_${this._manifest.integration_type}`
)
: this.hass.localize(
`ui.panel.config.integrations.integration_page.add_entry`
diff --git a/src/panels/config/integrations/integration-panels/zha/zha-add-devices-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-add-devices-page.ts
index 0201e8e4fc1e..baae1a21cf75 100644
--- a/src/panels/config/integrations/integration-panels/zha/zha-add-devices-page.ts
+++ b/src/panels/config/integrations/integration-panels/zha/zha-add-devices-page.ts
@@ -1,5 +1,4 @@
import "@material/mwc-button";
-import "@polymer/paper-input/paper-textarea";
import {
css,
CSSResultGroup,
@@ -20,6 +19,7 @@ import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant, Route } from "../../../../../types";
import { zhaTabs } from "./zha-config-dashboard";
import "./zha-device-pairing-status-card";
+import "../../../../../components/ha-textarea";
@customElement("zha-add-devices-page")
class ZHAAddDevicesPage extends LitElement {
@@ -146,13 +146,13 @@ class ZHAAddDevicesPage extends LitElement {
`}
${this._showLogs
- ? html`
- `
+ `
: ""}
`;
@@ -165,13 +165,6 @@ class ZHAAddDevicesPage extends LitElement {
private _handleMessage(message: any): void {
if (message.type === LOG_OUTPUT) {
this._formattedEvents += message.log_entry.message + "\n";
- if (this.shadowRoot) {
- const paperTextArea = this.shadowRoot.querySelector("paper-textarea");
- if (paperTextArea) {
- const textArea = (paperTextArea.inputElement as any).textarea;
- textArea.scrollTop = textArea.scrollHeight;
- }
- }
}
if (message.type && DEVICE_MESSAGE_TYPES.includes(message.type)) {
this._discoveredDevices[message.device_info.ieee] = message.device_info;
@@ -266,6 +259,9 @@ class ZHAAddDevicesPage extends LitElement {
color: grey;
padding-left: 16px;
}
+ ha-textarea {
+ width: 100%;
+ }
`,
];
}
diff --git a/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts
index 5cd32c78da32..367485563c1b 100644
--- a/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts
+++ b/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts
@@ -1,6 +1,4 @@
import "@material/mwc-button";
-import "@polymer/paper-input/paper-input";
-import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
@@ -14,8 +12,9 @@ import {
ZHAGroup,
} from "../../../../../data/zha";
import "../../../../../layouts/hass-subpage";
-import type { ValueChangedEvent, HomeAssistant } from "../../../../../types";
+import type { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
+import "../../../../../components/ha-textfield";
import "./zha-device-endpoint-data-table";
import type { ZHADeviceEndpointDataTable } from "./zha-device-endpoint-data-table";
@@ -31,6 +30,8 @@ export class ZHAAddGroupPage extends LitElement {
@state() private _groupName = "";
+ @state() private _groupId?: string;
+
@query("zha-device-endpoint-data-table", true)
private _zhaDevicesDataTable!: ZHADeviceEndpointDataTable;
@@ -66,14 +67,23 @@ export class ZHAAddGroupPage extends LitElement {
"ui.panel.config.zha.groups.create_group_details"
)}
-
+ >
+
+