20231227.0 (#19157)

This commit is contained in:
Bram Kragten 2023-12-27 17:29:11 +01:00 committed by GitHub
commit 9d9e789f4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
142 changed files with 6432 additions and 3090 deletions

View File

@ -24,6 +24,7 @@ body:
required: true required: true
- label: I have tried a different browser to see if it is related to my browser. - label: I have tried a different browser to see if it is related to my browser.
required: true 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 - type: markdown
attributes: attributes:
value: | value: |

14
.github/labeler.yml vendored
View File

@ -1,21 +1,31 @@
Build: Build:
- changed-files:
- any-glob-to-any-file:
- build-scripts/** - build-scripts/**
- .browserslistrc - .browserslistrc
- gulpfile.js - gulpfile.js
Cast: Cast:
- changed-files:
- any-glob-to-any-file:
- cast/src/** - cast/src/**
- src/cast/** - src/cast/**
Demo: Demo:
- changed-files:
- any-glob-to-any-file:
- demo/src/** - demo/src/**
- src/fake_data/** - src/fake_data/**
Design: Design:
- changed-files:
- any-glob-to-any-file:
- gallery/src/** - gallery/src/**
- src/fake_data/** - src/fake_data/**
Dependencies: Dependencies:
- changed-files:
- any-glob-to-any-file:
- package.json - package.json
- renovate.json - renovate.json
- yarn.lock - yarn.lock
@ -24,8 +34,12 @@ Dependencies:
- .nvmrc - .nvmrc
GitHub Actions: GitHub Actions:
- changed-files:
- any-glob-to-any-file:
- .github/workflows/** - .github/workflows/**
- .github/*.yml - .github/*.yml
Supervisor: Supervisor:
- changed-files:
- any-glob-to-any-file:
- hassio/src/** - hassio/src/**

View File

@ -26,7 +26,7 @@ jobs:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@ -62,7 +62,7 @@ jobs:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@ -57,14 +57,14 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install --immutable run: yarn install --immutable
- name: Build resources - 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 - name: Run Tests
run: yarn run test run: yarn run test
build: build:
@ -75,7 +75,7 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@ -99,7 +99,7 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

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

View File

@ -27,7 +27,7 @@ jobs:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@ -63,7 +63,7 @@ jobs:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Apply labels - name: Apply labels
uses: actions/labeler@v4.3.0 uses: actions/labeler@v5.0.0
with: with:
sync-labels: true sync-labels: true

View File

@ -23,12 +23,12 @@ jobs:
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@ -29,12 +29,12 @@ jobs:
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 90 days stale policy - name: 90 days stale policy
uses: actions/stale@v8.0.0 uses: actions/stale@v9.0.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90 days-before-stale: 90

View File

@ -1,6 +1,7 @@
const path = require("path"); const path = require("path");
const env = require("./env.cjs"); const env = require("./env.cjs");
const paths = require("./paths.cjs"); const paths = require("./paths.cjs");
const { dependencies } = require("../package.json");
// GitHub base URL to use for production source maps // 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 // 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", "@babel/preset-env",
{ {
useBuiltIns: latestBuild ? false : "usage", useBuiltIns: latestBuild ? false : "usage",
corejs: latestBuild ? false : "3.33", corejs: latestBuild ? false : dependencies["core-js"],
bugfixes: true, bugfixes: true,
shippedProposals: true, shippedProposals: true,
}, },
@ -140,7 +141,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
// Import helpers and regenerator from runtime package // Import helpers and regenerator from runtime package
[ [
"@babel/plugin-transform-runtime", "@babel/plugin-transform-runtime",
{ version: require("../package.json").dependencies["@babel/runtime"] }, { version: dependencies["@babel/runtime"] },
], ],
// Support some proposals still in TC39 process // Support some proposals still in TC39 process
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }], ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],

View File

@ -509,7 +509,7 @@ export default {
away_mode: "on", away_mode: "on",
aux_heat: "off", aux_heat: "off",
unit_of_measurement: "°C", unit_of_measurement: "°C",
friendly_name: "Hvac", friendly_name: "HVAC",
supported_features: 3833, supported_features: 3833,
}, },
last_changed: "2018-07-19T10:44:46.200650+00:00", last_changed: "2018-07-19T10:44:46.200650+00:00",

View File

@ -35,6 +35,18 @@ const ENTITIES = [
friendly_name: "Nest", friendly_name: "Nest",
supported_features: 43, 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", { getEntity("climate", "unavailable", "unavailable", {
supported_features: 43, supported_features: 43,
}), }),
@ -57,6 +69,23 @@ const CONFIGS = [
entity: climate.nest 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", heading: "Unavailable",
config: ` config: `

View File

@ -31,6 +31,21 @@ const ENTITIES = [
max_temp: 30, max_temp: 30,
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE, 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", { getEntity("climate", "hvac", "auto", {
friendly_name: "Basic hvac", friendly_name: "Basic hvac",
hvac_modes: ["auto", "off"], hvac_modes: ["auto", "off"],

View File

@ -1,12 +1,6 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card"; 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 "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { import {
@ -15,13 +9,14 @@ import {
} from "../../../../src/fake_data/provide_hass"; } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos"; import "../../components/demo-more-infos";
import { LONG_TEXT } from "../../data/text"; import { LONG_TEXT } from "../../data/text";
import { UpdateEntityFeature } from "../../../../src/data/update";
const base_attributes = { const base_attributes = {
title: "Awesome", title: "Awesome",
installed_version: "1.2.2", installed_version: "1.2.2",
latest_version: "1.2.3", latest_version: "1.2.3",
release_url: "https://home-assistant.io", release_url: "https://home-assistant.io",
supported_features: UPDATE_SUPPORT_INSTALL, supported_features: UpdateEntityFeature.INSTALL,
skipped_version: null, skipped_version: null,
in_progress: false, in_progress: false,
release_summary: release_summary:
@ -61,7 +56,7 @@ const ENTITIES = [
getEntity("update", "update7", "on", { getEntity("update", "update7", "on", {
...base_attributes, ...base_attributes,
supported_features: supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_BACKUP, base_attributes.supported_features + UpdateEntityFeature.BACKUP,
friendly_name: "With backup support", friendly_name: "With backup support",
}), }),
getEntity("update", "update8", "on", { getEntity("update", "update8", "on", {
@ -73,21 +68,21 @@ const ENTITIES = [
...base_attributes, ...base_attributes,
in_progress: 25, in_progress: 25,
supported_features: supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
friendly_name: "With 25 in_progress", friendly_name: "With 25 in_progress",
}), }),
getEntity("update", "update10", "on", { getEntity("update", "update10", "on", {
...base_attributes, ...base_attributes,
in_progress: 50, in_progress: 50,
supported_features: supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
friendly_name: "With 50 in_progress", friendly_name: "With 50 in_progress",
}), }),
getEntity("update", "update11", "on", { getEntity("update", "update11", "on", {
...base_attributes, ...base_attributes,
in_progress: 75, in_progress: 75,
supported_features: supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
friendly_name: "With 75 in_progress", friendly_name: "With 75 in_progress",
}), }),
getEntity("update", "update12", "unavailable", { getEntity("update", "update12", "unavailable", {
@ -114,19 +109,19 @@ const ENTITIES = [
...base_attributes, ...base_attributes,
friendly_name: "Update with release notes", friendly_name: "Update with release notes",
supported_features: supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES, base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES,
}), }),
getEntity("update", "update17", "off", { getEntity("update", "update17", "off", {
...base_attributes, ...base_attributes,
friendly_name: "Update with release notes error", friendly_name: "Update with release notes error",
supported_features: supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES, base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES,
}), }),
getEntity("update", "update18", "off", { getEntity("update", "update18", "off", {
...base_attributes, ...base_attributes,
friendly_name: "Update with release notes loading", friendly_name: "Update with release notes loading",
supported_features: supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES, base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES,
}), }),
getEntity("update", "update19", "on", { getEntity("update", "update19", "on", {
...base_attributes, ...base_attributes,
@ -142,9 +137,10 @@ const ENTITIES = [
getEntity("update", "update21", "on", { getEntity("update", "update21", "on", {
...base_attributes, ...base_attributes,
in_progress: true, 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: supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS, base_attributes.supported_features + UpdateEntityFeature.PROGRESS,
}), }),
]; ];

View File

@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-select"; import "../../../../src/components/ha-select";
import { import {
extractApiErrorMessage, extractApiErrorMessage,

View File

@ -25,15 +25,15 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.23.5", "@babel/runtime": "7.23.6",
"@braintree/sanitize-url": "6.0.4", "@braintree/sanitize-url": "7.0.0",
"@codemirror/autocomplete": "6.11.1", "@codemirror/autocomplete": "6.11.1",
"@codemirror/commands": "6.3.2", "@codemirror/commands": "6.3.2",
"@codemirror/language": "6.9.3", "@codemirror/language": "6.9.3",
"@codemirror/legacy-modes": "6.3.3", "@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.5", "@codemirror/search": "6.5.5",
"@codemirror/state": "6.3.2", "@codemirror/state": "6.3.3",
"@codemirror/view": "6.22.1", "@codemirror/view": "6.22.3",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.0", "@formatjs/intl-datetimeformat": "6.12.0",
"@formatjs/intl-displaynames": "6.6.4", "@formatjs/intl-displaynames": "6.6.4",
@ -80,7 +80,7 @@
"@material/mwc-top-app-bar": "0.27.0", "@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0", "@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.1", "@material/web": "=1.1.1",
"@mdi/js": "7.3.67", "@mdi/js": "7.3.67",
"@mdi/svg": "7.3.67", "@mdi/svg": "7.3.67",
"@polymer/paper-input": "3.2.1", "@polymer/paper-input": "3.2.1",
@ -90,8 +90,8 @@
"@polymer/paper-toast": "3.0.1", "@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1", "@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.2.5", "@vaadin/combo-box": "24.3.2",
"@vaadin/vaadin-themable-mixin": "24.2.5", "@vaadin/vaadin-themable-mixin": "24.3.2",
"@vibrant/color": "3.2.1-alpha.1", "@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1", "@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@ -101,16 +101,16 @@
"app-datepicker": "5.1.1", "app-datepicker": "5.1.1",
"chart.js": "4.4.1", "chart.js": "4.4.1",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.33.3", "core-js": "3.34.0",
"cropperjs": "1.6.1", "cropperjs": "1.6.1",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"date-fns-tz": "2.0.0", "date-fns-tz": "2.0.0",
"deep-clone-simple": "1.1.1", "deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"element-internals-polyfill": "1.3.9", "element-internals-polyfill": "1.3.10",
"fuse.js": "7.0.0", "fuse.js": "7.0.0",
"google-timezones-json": "1.2.0", "google-timezones-json": "1.2.0",
"hls.js": "1.4.12", "hls.js": "1.4.14",
"home-assistant-js-websocket": "9.1.0", "home-assistant-js-websocket": "9.1.0",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"intl-messageformat": "10.5.8", "intl-messageformat": "10.5.8",
@ -119,7 +119,7 @@
"leaflet-draw": "1.0.4", "leaflet-draw": "1.0.4",
"lit": "2.8.0", "lit": "2.8.0",
"luxon": "3.4.4", "luxon": "3.4.4",
"marked": "11.0.0", "marked": "11.1.0",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2", "proxy-polyfill": "0.3.2",
@ -138,7 +138,7 @@
"unfetch": "5.0.0", "unfetch": "5.0.0",
"vis-data": "7.1.9", "vis-data": "7.1.9",
"vis-network": "9.1.9", "vis-network": "9.1.9",
"vue": "2.7.15", "vue": "2.7.16",
"vue2-daterange-picker": "0.6.8", "vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0", "weekstart": "2.0.0",
"workbox-cacheable-response": "7.0.0", "workbox-cacheable-response": "7.0.0",
@ -150,22 +150,22 @@
"xss": "1.0.14" "xss": "1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.23.5", "@babel/core": "7.23.6",
"@babel/helper-define-polyfill-provider": "0.4.3", "@babel/helper-define-polyfill-provider": "0.4.4",
"@babel/plugin-proposal-decorators": "7.23.5", "@babel/plugin-proposal-decorators": "7.23.6",
"@babel/plugin-transform-runtime": "7.23.4", "@babel/plugin-transform-runtime": "7.23.6",
"@babel/preset-env": "7.23.5", "@babel/preset-env": "7.23.6",
"@babel/preset-typescript": "7.23.3", "@babel/preset-typescript": "7.23.3",
"@bundle-stats/plugin-webpack-filter": "4.8.3", "@bundle-stats/plugin-webpack-filter": "4.8.3",
"@koa/cors": "4.0.0", "@koa/cors": "5.0.0",
"@lokalise/node-api": "12.0.0", "@lokalise/node-api": "12.1.0",
"@octokit/auth-oauth-device": "6.0.1", "@octokit/auth-oauth-device": "6.0.1",
"@octokit/plugin-retry": "6.0.1", "@octokit/plugin-retry": "6.0.1",
"@octokit/rest": "20.0.2", "@octokit/rest": "20.0.2",
"@open-wc/dev-server-hmr": "0.1.4", "@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4", "@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "25.0.7", "@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-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.5", "@rollup/plugin-replace": "5.0.5",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
@ -184,22 +184,22 @@
"@types/tar": "6.1.10", "@types/tar": "6.1.10",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.13.2", "@typescript-eslint/eslint-plugin": "6.15.0",
"@typescript-eslint/parser": "6.13.2", "@typescript-eslint/parser": "6.15.0",
"@web/dev-server": "0.1.38", "@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1", "@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.10", "chai": "4.3.10",
"del": "7.1.0", "del": "7.1.0",
"eslint": "8.55.0", "eslint": "8.56.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0", "eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.8", "eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-disable": "2.0.3", "eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.29.0", "eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.10.1", "eslint-plugin-lit": "1.11.0",
"eslint-plugin-lit-a11y": "4.1.1", "eslint-plugin-lit-a11y": "4.1.1",
"eslint-plugin-unused-imports": "3.0.0", "eslint-plugin-unused-imports": "3.0.0",
"eslint-plugin-wc": "2.0.4", "eslint-plugin-wc": "2.0.4",
@ -217,19 +217,19 @@
"instant-mocha": "1.5.2", "instant-mocha": "1.5.2",
"jszip": "3.10.1", "jszip": "3.10.1",
"lint-staged": "15.2.0", "lint-staged": "15.2.0",
"lit-analyzer": "2.0.1", "lit-analyzer": "2.0.2",
"lodash.template": "4.5.0", "lodash.template": "4.5.0",
"magic-string": "0.30.5", "magic-string": "0.30.5",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"mocha": "10.2.0", "mocha": "10.2.0",
"object-hash": "3.0.0", "object-hash": "3.0.0",
"open": "9.1.0", "open": "10.0.1",
"pinst": "3.0.0", "pinst": "3.0.0",
"prettier": "3.1.0", "prettier": "3.1.1",
"rollup": "2.79.1", "rollup": "2.79.1",
"rollup-plugin-string": "3.0.0", "rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.10.0", "rollup-plugin-visualizer": "5.11.0",
"serve-handler": "6.1.5", "serve-handler": "6.1.5",
"sinon": "17.0.1", "sinon": "17.0.1",
"source-map-url": "0.4.1", "source-map-url": "0.4.1",
@ -237,7 +237,7 @@
"tar": "6.2.0", "tar": "6.2.0",
"terser-webpack-plugin": "5.3.9", "terser-webpack-plugin": "5.3.9",
"ts-lit-plugin": "2.0.1", "ts-lit-plugin": "2.0.1",
"typescript": "5.3.2", "typescript": "5.3.3",
"vinyl-buffer": "1.0.1", "vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0", "vinyl-source-stream": "2.0.0",
"webpack": "5.89.0", "webpack": "5.89.0",
@ -245,7 +245,7 @@
"webpack-dev-server": "4.15.1", "webpack-dev-server": "4.15.1",
"webpack-manifest-plugin": "5.0.0", "webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "5.0.2", "webpackbar": "6.0.0",
"workbox-build": "7.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", "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20231208.2" version = "20231227.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -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;
}
);

View File

@ -28,10 +28,12 @@ import {
mdiLockAlert, mdiLockAlert,
mdiLockClock, mdiLockClock,
mdiLockOpen, mdiLockOpen,
mdiMeterGas,
mdiMotionSensor, mdiMotionSensor,
mdiPackage, mdiPackage,
mdiPackageDown, mdiPackageDown,
mdiPackageUp, mdiPackageUp,
mdiPipeValve,
mdiPowerPlug, mdiPowerPlug,
mdiPowerPlugOff, mdiPowerPlugOff,
mdiRestart, mdiRestart,
@ -274,6 +276,16 @@ export const domainIconWithoutDefault = (
: mdiPackageUp : mdiPackageUp
: mdiPackage; : mdiPackage;
case "valve":
switch (stateObj?.attributes.device_class) {
case "water":
return mdiPipeValve;
case "gas":
return mdiMeterGas;
default:
return mdiPipeValve;
}
case "water_heater": case "water_heater":
return compareState === "off" ? mdiWaterBoilerOff : mdiWaterBoiler; return compareState === "off" ? mdiWaterBoilerOff : mdiWaterBoiler;

View File

@ -42,6 +42,8 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
return compareState !== "standby"; return compareState !== "standby";
case "vacuum": case "vacuum":
return !["idle", "docked", "paused"].includes(compareState); return !["idle", "docked", "paused"].includes(compareState);
case "valve":
return compareState !== "closed";
case "plant": case "plant":
return compareState === "problem"; return compareState === "problem";
case "group": case "group":

View File

@ -37,6 +37,7 @@ const STATE_COLORED_DOMAIN = new Set([
"timer", "timer",
"update", "update",
"vacuum", "vacuum",
"valve",
"water_heater", "water_heater",
]); ]);

View File

@ -101,7 +101,8 @@ export class StatisticsChart extends LitElement {
changedProps.has("unit") || changedProps.has("unit") ||
changedProps.has("period") || changedProps.has("period") ||
changedProps.has("chartType") || changedProps.has("chartType") ||
changedProps.has("logarithmicScale") changedProps.has("logarithmicScale") ||
changedProps.has("hideLegend")
) { ) {
this._createOptions(); this._createOptions();
} }

View File

@ -5,6 +5,10 @@ import DateRangePicker from "vue2-daterange-picker";
// @ts-ignore // @ts-ignore
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css"; import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
import { fireEvent } from "../common/dom/fire_event"; 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 // Set the current date to the left picker instead of the right picker because the right is hidden
const CustomDateRangePicker = Vue.extend({ const CustomDateRangePicker = Vue.extend({
@ -63,6 +67,10 @@ const Component = Vue.extend({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
language: {
type: String,
default: "en",
},
}, },
render(createElement) { render(createElement) {
// @ts-expect-error // @ts-expect-error
@ -77,6 +85,8 @@ const Component = Vue.extend({
ranges: this.ranges ? {} : false, ranges: this.ranges ? {} : false,
"locale-data": { "locale-data": {
firstDay: this.firstDay, firstDay: this.firstDay,
daysOfWeek: localizeWeekdays(this.language, true),
monthNames: localizeMonths(this.language, false),
}, },
}, },
model: { model: {
@ -145,6 +155,8 @@ class DateRangePickerElement extends WrappedElement {
); );
color: var(--primary-text-color); color: var(--primary-text-color);
min-width: initial !important; min-width: initial !important;
max-height: var(--date-range-picker-max-height);
overflow-y: auto;
} }
.daterangepicker:before { .daterangepicker:before {
display: none; display: none;
@ -162,7 +174,7 @@ class DateRangePickerElement extends WrappedElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
border-radius: 0; border-radius: 0;
outline: none; outline: none;
width: 32px; min-width: 32px;
height: 32px; height: 32px;
} }
.daterangepicker td.off, .daterangepicker td.off,
@ -238,6 +250,9 @@ class DateRangePickerElement extends WrappedElement {
} }
.daterangepicker .drp-calendar.left { .daterangepicker .drp-calendar.left {
padding: 8px; padding: 8px;
width: unset;
max-width: unset;
min-width: 270px;
} }
.daterangepicker.show-calendar .ranges { .daterangepicker.show-calendar .ranges {
margin-top: 0; margin-top: 0;

View File

@ -446,6 +446,7 @@ export class HaAreaPicker extends LitElement {
cancel: () => { cancel: () => {
this._setValue(undefined); this._setValue(undefined);
this._suggestion = undefined; this._suggestion = undefined;
this.comboBox.setInputValue("");
}, },
}); });
} }

View File

@ -86,6 +86,7 @@ export class HaBigNumber extends LitElement {
.value .decimal { .value .decimal {
font-size: 0.42em; font-size: 0.42em;
line-height: 1.33; line-height: 1.33;
min-height: 1.33em;
} }
.value .unit { .value .unit {
font-size: 0.33em; font-size: 0.33em;

View File

@ -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 as controlStyles } from "@material/mwc-list/mwc-control-list-item.css";
import { styles } from "@material/mwc-list/mwc-list-item.css"; import { styles } from "@material/mwc-list/mwc-list-item.css";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-check-list-item") @customElement("ha-check-list-item")
export class HaCheckListItem extends CheckListItemBase { export class HaCheckListItem extends CheckListItemBase {
async onChange(event) {
super.onChange(event);
fireEvent(this, event.type);
}
static override styles = [ static override styles = [
styles, styles,
controlStyles, controlStyles,
@ -22,6 +28,15 @@ export class HaCheckListItem extends CheckListItemBase {
margin-inline-start: 0px; margin-inline-start: 0px;
direction: var(--direction); 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);
}
`, `,
]; ];
} }

View File

@ -180,7 +180,7 @@ export class HaComboBox extends LitElement {
></div>`} ></div>`}
.icon=${this.icon} .icon=${this.icon}
.invalid=${this.invalid} .invalid=${this.invalid}
helper=${ifDefined(this.helper)} .helper=${this.helper}
helperPersistent helperPersistent
> >
<slot name="icon" slot="leadingIcon"></slot> <slot name="icon" slot="leadingIcon"></slot>

View File

@ -253,6 +253,7 @@ export class HaDateRangePicker extends LitElement {
opening-direction=${this.openingDirection || opening-direction=${this.openingDirection ||
this._calcedOpeningDirection} this._calcedOpeningDirection}
first-day=${firstWeekdayIndex(this.hass.locale)} first-day=${firstWeekdayIndex(this.hass.locale)}
language=${this.hass.locale.language}
> >
<div slot="input" class="date-range-inputs" @click=${this._handleClick}> <div slot="input" class="date-range-inputs" @click=${this._handleClick}>
${!this.minimal ${!this.minimal

View File

@ -13,13 +13,15 @@ export const createCloseHeading = (
hass: HomeAssistant | undefined, hass: HomeAssistant | undefined,
title: string | TemplateResult title: string | TemplateResult
) => html` ) => html`
<div class="header_title">${title}</div> <div class="header_title">
<span>${title}</span>
<ha-icon-button <ha-icon-button
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"} .label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
.path=${mdiClose} .path=${mdiClose}
dialogAction="close" dialogAction="close"
class="header_button" class="header_button"
></ha-icon-button> ></ha-icon-button>
</div>
`; `;
@customElement("ha-dialog") @customElement("ha-dialog")
@ -94,15 +96,12 @@ export class HaDialog extends DialogBase {
} }
.mdc-dialog__title { .mdc-dialog__title {
padding: 24px 24px 0 24px; padding: 24px 24px 0 24px;
text-overflow: ellipsis;
overflow: hidden;
} }
.mdc-dialog__actions { .mdc-dialog__actions {
padding: 12px 24px 12px 24px; padding: 12px 24px 12px 24px;
} }
.mdc-dialog__title::before { .mdc-dialog__title::before {
display: block; content: unset;
height: 0px;
} }
.mdc-dialog .mdc-dialog__content { .mdc-dialog .mdc-dialog__content {
position: var(--dialog-content-position, relative); position: var(--dialog-content-position, relative);
@ -126,19 +125,26 @@ export class HaDialog extends DialogBase {
flex-direction: column; flex-direction: column;
} }
.header_title { .header_title {
margin-right: 32px; position: relative;
margin-inline-end: 32px; padding-right: 40px;
margin-inline-start: initial; padding-inline-end: 40px;
padding-inline-start: initial;
direction: var(--direction); direction: var(--direction);
} }
.header_title span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
.header_button { .header_button {
position: absolute; position: absolute;
right: 16px; right: -8px;
top: 14px; top: -8px;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
inset-inline-start: initial; inset-inline-start: initial;
inset-inline-end: 16px; inset-inline-end: -8px;
direction: var(--direction); direction: var(--direction);
} }
.dialog-actions { .dialog-actions {

View File

@ -36,17 +36,24 @@ export class HaListItem extends ListItemBase {
--mdc-list-item-graphic-margin, --mdc-list-item-graphic-margin,
16px 16px
) !important; ) !important;
direction: var(--direction); direction: var(--direction) !important;
} }
span.material-icons:last-of-type { span.material-icons:last-of-type {
margin-inline-start: auto !important; margin-inline-start: auto !important;
margin-inline-end: 0px !important; margin-inline-end: 0px !important;
direction: var(--direction); direction: var(--direction) !important;
} }
.mdc-deprecated-list-item__meta { .mdc-deprecated-list-item__meta {
display: var(--mdc-list-item-meta-display); display: var(--mdc-list-item-meta-display);
align-items: center; align-items: center;
} }
:host([graphic="icon"]:not([twoline]))
.mdc-deprecated-list-item__graphic {
margin-inline-end: var(
--mdc-list-item-graphic-margin,
20px
) !important;
}
:host([multiline-secondary]) { :host([multiline-secondary]) {
height: auto; height: auto;
} }
@ -78,6 +85,15 @@ export class HaListItem extends ListItemBase {
pointer-events: unset; 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``,
]; ];
} }
} }

View File

@ -95,6 +95,15 @@ class HaMarkdownElement extends ReactiveElement {
} }
node.firstElementChild!.replaceWith(alertNote); 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}`
);
} }
} }
} }

View File

@ -2,11 +2,6 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "./ha-markdown-element"; 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") @customElement("ha-markdown")
export class HaMarkdown extends LitElement { export class HaMarkdown extends LitElement {
@property() public content?; @property() public content?;

View File

@ -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`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html`<canvas></canvas>`;
}
static styles = css`
:host {
display: block;
}
`;
}

View File

@ -43,6 +43,22 @@ export class HaNumberSelector extends LitElement {
this.selector.number?.min === undefined || this.selector.number?.min === undefined ||
this.selector.number?.max === 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` return html`
<div class="input"> <div class="input">
${!isBox ${!isBox
@ -52,12 +68,10 @@ export class HaNumberSelector extends LitElement {
: ""} : ""}
<ha-slider <ha-slider
labeled labeled
.min=${this.selector.number?.min} .min=${this.selector.number!.min}
.max=${this.selector.number?.max} .max=${this.selector.number!.max}
.value=${this.value ?? ""} .value=${this.value ?? ""}
.step=${this.selector.number?.step === "any" .step=${sliderStep}
? undefined
: this.selector.number?.step ?? 1}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
@change=${this._handleSliderChange} @change=${this._handleSliderChange}

View File

@ -70,15 +70,15 @@ const SELECTOR_SCHEMAS = {
number: [ number: [
{ {
name: "min", name: "min",
selector: { number: { mode: "box" } }, selector: { number: { mode: "box", step: "any" } },
}, },
{ {
name: "max", name: "max",
selector: { number: { mode: "box" } }, selector: { number: { mode: "box", step: "any" } },
}, },
{ {
name: "step", name: "step",
selector: { number: { mode: "box" } }, selector: { number: { mode: "box", step: "any" } },
}, },
] as const, ] as const,
object: [] as const, object: [] as const,

View File

@ -522,6 +522,14 @@ export class HaServiceControl extends LitElement {
defaultValue = field.selector.constant?.value; defaultValue = field.selector.constant?.value;
} }
if (
defaultValue == null &&
field?.selector &&
"boolean" in field.selector
) {
defaultValue = false;
}
if (defaultValue != null) { if (defaultValue != null) {
data = { data = {
...this._value?.data, ...this._value?.data,
@ -597,9 +605,9 @@ export class HaServiceControl extends LitElement {
); );
} }
target = { target = {
entity_id: targetEntities, ...(targetEntities.length ? { entity_id: targetEntities } : {}),
device_id: targetDevices, ...(targetDevices.length ? { device_id: targetDevices } : {}),
area_id: targetAreas, ...(targetAreas.length ? { area_id: targetAreas } : {}),
}; };
} }
} }

View File

@ -35,7 +35,12 @@ export class HaSettingsRow extends LitElement {
align-items: center; align-items: center;
} }
.body { .body {
padding: 8px 16px 8px 0; padding-top: 8px;
padding-bottom: 8px;
padding-left: 0;
padding-inline-start: 0;
padding-right: 16x;
padding-inline-end: 16px;
overflow: hidden; overflow: hidden;
display: var(--layout-vertical_-_display); display: var(--layout-vertical_-_display);
flex-direction: var(--layout-vertical_-_flex-direction); flex-direction: var(--layout-vertical_-_flex-direction);

View File

@ -11,6 +11,7 @@ export class HaSlider extends MdSlider {
:host { :host {
--md-sys-color-primary: var(--primary-color); --md-sys-color-primary: var(--primary-color);
--md-sys-color-outline: var(--outline-color); --md-sys-color-outline: var(--outline-color);
--md-sys-color-on-surface: var(--primary-text-color);
--md-slider-handle-width: 14px; --md-slider-handle-width: 14px;
--md-slider-handle-height: 14px; --md-slider-handle-height: 14px;
min-width: 100px; min-width: 100px;

View File

@ -280,6 +280,8 @@ export abstract class TopAppBarBaseBase extends BaseElement {
} }
#title { #title {
border-right: 1px solid rgba(255, 255, 255, 0.12); border-right: 1px solid rgba(255, 255, 255, 0.12);
border-inline-end: 1px solid rgba(255, 255, 255, 0.12);
border-inline-start: initial;
box-sizing: border-box; box-sizing: border-box;
flex: 0 0 var(--sidepane-width, 250px); flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px); width: var(--sidepane-width, 250px);
@ -290,6 +292,8 @@ export abstract class TopAppBarBaseBase extends BaseElement {
} }
.pane { .pane {
border-right: 1px solid var(--divider-color); border-right: 1px solid var(--divider-color);
border-inline-end: 1px solid var(--divider-color);
border-inline-start: initial;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex: 0 0 var(--sidepane-width, 250px); flex: 0 0 var(--sidepane-width, 250px);

View File

@ -0,0 +1,102 @@
import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js";
import { CSSResultGroup, LitElement, html, css, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { supportsFeature } from "../common/entity/supports-feature";
import {
ValveEntity,
ValveEntityFeature,
canClose,
canOpen,
canStop,
} from "../data/valve";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
@customElement("ha-valve-controls")
class HaValveControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: ValveEntity;
protected render() {
if (!this.stateObj) {
return nothing;
}
return html`
<div class="state">
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.OPEN),
})}
.label=${this.hass.localize("ui.card.valve.open_valve")}
@click=${this._onOpenTap}
.disabled=${!canOpen(this.stateObj)}
.path=${mdiValveOpen}
>
</ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.STOP),
})}
.label=${this.hass.localize("ui.card.valve.stop_valve")}
@click=${this._onStopTap}
.disabled=${!canStop(this.stateObj)}
.path=${mdiStop}
></ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.CLOSE),
})}
.label=${this.hass.localize("ui.card.valve.close_valve")}
@click=${this._onCloseTap}
.disabled=${!canClose(this.stateObj)}
.path=${mdiValveClosed}
>
</ha-icon-button>
</div>
`;
}
private _onOpenTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "open_valve", {
entity_id: this.stateObj.entity_id,
});
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "close_valve", {
entity_id: this.stateObj.entity_id,
});
}
private _onStopTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "stop_valve", {
entity_id: this.stateObj.entity_id,
});
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
.state {
white-space: nowrap;
}
.hidden {
visibility: hidden !important;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-valve-controls": HaValveControls;
}
}

View File

@ -1,5 +1,12 @@
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
@ -18,6 +25,12 @@ import { traceTabStyles } from "./trace-tab-styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph"; import type { NodeInfo } from "./hat-script-graph";
const TRACE_PATH_TABS = [
"step_config",
"changed_variables",
"logbook",
] as const;
@customElement("ha-trace-path-details") @customElement("ha-trace-path-details")
export class HaTracePathDetails extends LitElement { export class HaTracePathDetails extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -34,7 +47,7 @@ export class HaTracePathDetails extends LitElement {
@property() public trackedNodes!: Record<string, any>; @property() public trackedNodes!: Record<string, any>;
@state() private _view: "config" | "changed_variables" | "logbook" = "config"; @state() private _view: (typeof TRACE_PATH_TABS)[number] = "step_config";
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
@ -43,23 +56,21 @@ export class HaTracePathDetails extends LitElement {
</div> </div>
<div class="tabs top"> <div class="tabs top">
${[ ${TRACE_PATH_TABS.map(
["config", "Step Config"], (view) => html`
["changed_variables", "Changed Variables"],
["logbook", "Related logbook entries"],
].map(
([view, label]) => html`
<button <button
.view=${view} .view=${view}
class=${classMap({ active: this._view === view })} class=${classMap({ active: this._view === view })}
@click=${this._showTab} @click=${this._showTab}
> >
${label} ${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.${view}`
)}
</button> </button>
` `
)} )}
</div> </div>
${this._view === "config" ${this._view === "step_config"
? this._renderSelectedConfig() ? this._renderSelectedConfig()
: this._view === "changed_variables" : this._view === "changed_variables"
? this._renderChangedVars() ? this._renderChangedVars()
@ -71,7 +82,9 @@ export class HaTracePathDetails extends LitElement {
const paths = this.trace.trace; const paths = this.trace.trace;
if (!this.selected?.path) { if (!this.selected?.path) {
return "Select a node on the left for more information."; return this.hass!.localize(
"ui.panel.config.automation.trace.path.choose"
);
} }
// HACK: default choice node is not part of paths. We filter them out here by checking parent. // HACK: default choice node is not part of paths. We filter them out here by checking parent.
@ -82,12 +95,16 @@ export class HaTracePathDetails extends LitElement {
] as ChooseActionTraceStep[]; ] as ChooseActionTraceStep[];
if (parentTraceInfo && parentTraceInfo[0]?.result?.choice === "default") { if (parentTraceInfo && parentTraceInfo[0]?.result?.choice === "default") {
return "The default action was executed because no options matched."; return this.hass!.localize(
"ui.panel.config.automation.trace.path.default_action_executed"
);
} }
} }
if (!(this.selected.path in paths)) { if (!(this.selected.path in paths)) {
return "This node was not executed and so no further trace information is available."; return this.hass!.localize(
"ui.panel.config.automation.trace.path.no_further_execution"
);
} }
const parts: TemplateResult[][] = []; const parts: TemplateResult[][] = [];
@ -115,29 +132,53 @@ export class HaTracePathDetails extends LitElement {
trace as any; trace as any;
if (result?.enabled === false) { if (result?.enabled === false) {
return html`This node was disabled and skipped during execution so return html`${this.hass!.localize(
no further trace information is available.`; "ui.panel.config.automation.trace.path.disabled_node"
)}`;
} }
return html` return html`
${curPath === this.selected.path ${curPath === this.selected.path
? "" ? ""
: html`<h2>${curPath.substr(this.selected.path.length + 1)}</h2>`} : html`<h2>
${data.length === 1 ? "" : html`<h3>Iteration ${idx + 1}</h3>`} ${curPath.substring(this.selected.path.length + 1)}
Executed: </h2>`}
${formatDateTimeWithSeconds( ${data.length === 1
? nothing
: html`<h3>
${this.hass!.localize(
"ui.panel.config.automation.trace.path.iteration",
{ number: idx + 1 }
)}
</h3>`}
${this.hass!.localize(
"ui.panel.config.automation.trace.path.executed",
{
time: formatDateTimeWithSeconds(
new Date(timestamp), new Date(timestamp),
this.hass.locale, this.hass.locale,
this.hass.config this.hass.config
)}<br /> ),
}
)}
<br />
${result ${result
? html`Result: ? html`${this.hass!.localize(
"ui.panel.config.automation.trace.path.result"
)}
<pre>${dump(result)}</pre>` <pre>${dump(result)}</pre>`
: error : error
? html`<div class="error">Error: ${error}</div>` ? html`<div class="error">
: ""} ${this.hass!.localize(
"ui.panel.config.automation.trace.path.error",
{
error: error,
}
)}
</div>`
: nothing}
${Object.keys(rest).length === 0 ${Object.keys(rest).length === 0
? "" ? nothing
: html`<pre>${dump(rest)}</pre>`} : html`<pre>${dump(rest)}</pre>`}
`; `;
}) })
@ -149,16 +190,18 @@ export class HaTracePathDetails extends LitElement {
private _renderSelectedConfig() { private _renderSelectedConfig() {
if (!this.selected?.path) { if (!this.selected?.path) {
return ""; return nothing;
} }
const config = getDataFromPath(this.trace!.config, this.selected.path); const config = getDataFromPath(this.trace!.config, this.selected.path);
return config return config
? html`<ha-code-editor ? html`<ha-code-editor
.value=${dump(config).trimRight()} .value=${dump(config).trimEnd()}
readOnly readOnly
dir="ltr" dir="ltr"
></ha-code-editor>` ></ha-code-editor>`
: "Unable to find config"; : this.hass!.localize(
"ui.panel.config.automation.trace.path.unable_to_find_config"
);
} }
private _renderChangedVars() { private _renderChangedVars() {
@ -169,10 +212,19 @@ export class HaTracePathDetails extends LitElement {
<div class="padded-box"> <div class="padded-box">
${data.map( ${data.map(
(trace, idx) => html` (trace, idx) => html`
${idx > 0 ? html`<p>Iteration ${idx + 1}</p>` : ""} ${idx > 0
? html`<p>
${this.hass!.localize(
"ui.panel.config.automation.trace.path.iteration",
{ number: idx + 1 }
)}
</p>`
: ""}
${Object.keys(trace.changed_variables || {}).length === 0 ${Object.keys(trace.changed_variables || {}).length === 0
? "No variables changed" ? this.hass!.localize(
: html`<pre>${dump(trace.changed_variables).trimRight()}</pre>`} "ui.panel.config.automation.trace.path.no_variables_changed"
)
: html`<pre>${dump(trace.changed_variables).trimEnd()}</pre>`}
` `
)} )}
</div> </div>
@ -186,7 +238,11 @@ export class HaTracePathDetails extends LitElement {
const index = trackedPaths.indexOf(this.selected.path); const index = trackedPaths.indexOf(this.selected.path);
if (index === -1) { if (index === -1) {
return html`<div class="padded-box">Node not tracked.</div>`; return html`<div class="padded-box">
${this.hass!.localize(
"ui.panel.config.automation.trace.path.node_not_tracked"
)}
</div>`;
} }
let entries: LogbookEntry[]; let entries: LogbookEntry[];
@ -234,7 +290,9 @@ export class HaTracePathDetails extends LitElement {
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note> <hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
` `
: html`<div class="padded-box"> : html`<div class="padded-box">
No Logbook entries found for this step. ${this.hass!.localize(
"ui.panel.config.automation.trace.path.no_logbook_entries"
)}
</div>`; </div>`;
} }

View File

@ -125,10 +125,6 @@ export class HatGraphNode extends LitElement {
:host([notEnabled]:hover) circle { :host([notEnabled]:hover) circle {
--stroke-clr: var(--disabled-hover-clr); --stroke-clr: var(--disabled-hover-clr);
} }
svg {
width: 100%;
height: 100%;
}
circle, circle,
path.connector { path.connector {
stroke: var(--stroke-clr); stroke: var(--stroke-clr);

View File

@ -5,6 +5,8 @@ import {
mdiCallSplit, mdiCallSplit,
mdiCodeBraces, mdiCodeBraces,
mdiDevices, mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiGestureDoubleTap, mdiGestureDoubleTap,
mdiHandBackRight, mdiHandBackRight,
mdiPalette, mdiPalette,
@ -13,10 +15,12 @@ import {
mdiRoomService, mdiRoomService,
mdiShuffleDisabled, mdiShuffleDisabled,
mdiTimerOutline, mdiTimerOutline,
mdiTools,
mdiTrafficLight, mdiTrafficLight,
} from "@mdi/js"; } from "@mdi/js";
import { AutomationElementGroup } from "./automation";
export const ACTION_TYPES = { export const ACTION_ICONS = {
condition: mdiAbTesting, condition: mdiAbTesting,
delay: mdiTimerOutline, delay: mdiTimerOutline,
event: mdiGestureDoubleTap, event: mdiGestureDoubleTap,
@ -34,6 +38,43 @@ export const ACTION_TYPES = {
variables: mdiApplicationVariableOutline, variables: mdiApplicationVariableOutline,
} as const; } as const;
export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_TYPES>([ export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_ICONS>([
"variables", "variables",
]); ]);
export const ACTION_GROUPS: AutomationElementGroup = {
device_id: {},
helpers: {
icon: mdiTools,
members: {},
},
building_blocks: {
icon: mdiExcavator,
members: {
condition: {},
delay: {},
wait_template: {},
wait_for_trigger: {},
repeat: {},
choose: {},
if: {},
stop: {},
parallel: {},
variables: {},
},
},
other: {
icon: mdiDotsHorizontal,
members: {
event: {},
},
},
} as const;
export const SERVICE_PREFIX = "__SERVICE__";
export const isService = (key: string | undefined): boolean | undefined =>
key?.startsWith(SERVICE_PREFIX);
export const getService = (key: string): string =>
key.substring(SERVICE_PREFIX.length);

View File

@ -128,7 +128,7 @@ export const areaCompare =
(entries?: HomeAssistant["areas"], order?: string[]) => (entries?: HomeAssistant["areas"], order?: string[]) =>
(a: string, b: string) => { (a: string, b: string) => {
const indexA = order ? order.indexOf(a) : -1; const indexA = order ? order.indexOf(a) : -1;
const indexB = order ? order.indexOf(b) : 1; const indexB = order ? order.indexOf(b) : -1;
if (indexA === -1 && indexB === -1) { if (indexA === -1 && indexB === -1) {
const nameA = entries?.[a]?.name ?? a; const nameA = entries?.[a]?.name ?? a;
const nameB = entries?.[b]?.name ?? b; const nameB = entries?.[b]?.name ?? b;

View File

@ -275,6 +275,10 @@ export interface ShorthandNotCondition extends ShorthandBaseCondition {
not: Condition[]; not: Condition[];
} }
export interface AutomationElementGroup {
[key: string]: { icon?: string; members?: AutomationElementGroup };
}
export type Condition = export type Condition =
| StateCondition | StateCondition
| NumericStateCondition | NumericStateCondition

View File

@ -766,48 +766,38 @@ const tryDescribeCondition = (
// State Condition // State Condition
if (condition.condition === "state") { if (condition.condition === "state") {
let base = "Confirm";
if (!condition.entity_id) { if (!condition.entity_id) {
return `${base} state`; return hass.localize(
`${conditionsTranslationBaseKey}.state.description.no_entity`
);
} }
let attribute = "";
if (condition.attribute) { if (condition.attribute) {
const stateObj = Array.isArray(condition.entity_id) const stateObj = Array.isArray(condition.entity_id)
? hass.states[condition.entity_id[0]] ? hass.states[condition.entity_id[0]]
: hass.states[condition.entity_id]; : hass.states[condition.entity_id];
base += ` ${computeAttributeNameDisplay( attribute = computeAttributeNameDisplay(
hass.localize, hass.localize,
stateObj, stateObj,
hass.entities, hass.entities,
condition.attribute condition.attribute
)} of`; );
} }
if (Array.isArray(condition.entity_id)) {
const entities: string[] = []; const entities: string[] = [];
if (Array.isArray(condition.entity_id)) {
for (const entity of condition.entity_id.values()) { for (const entity of condition.entity_id.values()) {
if (hass.states[entity]) { if (hass.states[entity]) {
entities.push(computeStateName(hass.states[entity]) || entity); entities.push(computeStateName(hass.states[entity]) || entity);
} }
} }
if (entities.length !== 0) {
const entitiesString =
condition.match === "any"
? formatListWithOrs(hass.locale, entities)
: formatListWithAnds(hass.locale, entities);
base += ` ${entitiesString} ${
condition.entity_id.length > 1 ? "are" : "is"
}`;
} else {
// no entity_id or empty array
base += " an entity";
}
} else if (condition.entity_id) { } else if (condition.entity_id) {
base += ` ${ entities.push(
hass.states[condition.entity_id] hass.states[condition.entity_id]
? computeStateName(hass.states[condition.entity_id]) ? computeStateName(hass.states[condition.entity_id])
: condition.entity_id : condition.entity_id
} is`; );
} }
const states: string[] = []; const states: string[] = [];
@ -845,21 +835,27 @@ const tryDescribeCondition = (
); );
} }
if (states.length === 0) { let duration = "";
states.push("a state");
}
const statesString = formatListWithOrs(hass.locale, states);
base += ` ${statesString}`;
if (condition.for) { if (condition.for) {
const duration = describeDuration(hass.locale, condition.for); duration = describeDuration(hass.locale, condition.for) || "";
if (duration) {
base += ` for ${duration}`;
}
} }
return base; return hass.localize(
`${conditionsTranslationBaseKey}.state.description.full`,
{
hasAttribute: attribute !== "" ? "true" : "false",
attribute: attribute,
numberOfEntities: entities.length,
entities:
condition.match === "any"
? formatListWithOrs(hass.locale, entities)
: formatListWithAnds(hass.locale, entities),
numberOfStates: states.length,
states: formatListWithOrs(hass.locale, states),
hasDuration: duration !== "" ? "true" : "false",
duration: duration,
}
);
} }
// Numeric State Condition // Numeric State Condition

View File

@ -3,16 +3,21 @@ import {
mdiClockOutline, mdiClockOutline,
mdiCodeBraces, mdiCodeBraces,
mdiDevices, mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiGateOr, mdiGateOr,
mdiIdentifier, mdiIdentifier,
mdiMapClock,
mdiMapMarkerRadius, mdiMapMarkerRadius,
mdiNotEqualVariant, mdiNotEqualVariant,
mdiNumeric, mdiNumeric,
mdiShape,
mdiStateMachine, mdiStateMachine,
mdiWeatherSunny, mdiWeatherSunny,
} from "@mdi/js"; } from "@mdi/js";
import { AutomationElementGroup } from "./automation";
export const CONDITION_TYPES = { export const CONDITION_ICONS = {
device: mdiDevices, device: mdiDevices,
and: mdiAmpersand, and: mdiAmpersand,
or: mdiGateOr, or: mdiGateOr,
@ -25,3 +30,23 @@ export const CONDITION_TYPES = {
trigger: mdiIdentifier, trigger: mdiIdentifier,
zone: mdiMapMarkerRadius, zone: mdiMapMarkerRadius,
}; };
export const CONDITION_GROUPS: AutomationElementGroup = {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { sun: {}, time: {}, zone: {} },
},
building_blocks: {
icon: mdiExcavator,
members: { and: {}, or: {}, not: {} },
},
other: {
icon: mdiDotsHorizontal,
members: {
template: {},
trigger: {},
},
},
} as const;

View File

@ -65,7 +65,7 @@ export function canOpen(stateObj: CoverEntity) {
return false; return false;
} }
const assumedState = stateObj.attributes.assumed_state === true; const assumedState = stateObj.attributes.assumed_state === true;
return (!isFullyOpen(stateObj) && !isOpening(stateObj)) || assumedState; return assumedState || (!isFullyOpen(stateObj) && !isOpening(stateObj));
} }
export function canClose(stateObj: CoverEntity): boolean { export function canClose(stateObj: CoverEntity): boolean {
@ -73,7 +73,7 @@ export function canClose(stateObj: CoverEntity): boolean {
return false; return false;
} }
const assumedState = stateObj.attributes.assumed_state === true; const assumedState = stateObj.attributes.assumed_state === true;
return (!isFullyClosed(stateObj) && !isClosing(stateObj)) || assumedState; return assumedState || (!isFullyClosed(stateObj) && !isClosing(stateObj));
} }
export function canStop(stateObj: CoverEntity): boolean { export function canStop(stateObj: CoverEntity): boolean {
@ -85,7 +85,7 @@ export function canOpenTilt(stateObj: CoverEntity): boolean {
return false; return false;
} }
const assumedState = stateObj.attributes.assumed_state === true; const assumedState = stateObj.attributes.assumed_state === true;
return !isFullyOpenTilt(stateObj) || assumedState; return assumedState || !isFullyOpenTilt(stateObj);
} }
export function canCloseTilt(stateObj: CoverEntity): boolean { export function canCloseTilt(stateObj: CoverEntity): boolean {
@ -93,7 +93,7 @@ export function canCloseTilt(stateObj: CoverEntity): boolean {
return false; return false;
} }
const assumedState = stateObj.attributes.assumed_state === true; const assumedState = stateObj.attributes.assumed_state === true;
return !isFullyClosedTilt(stateObj) || assumedState; return assumedState || !isFullyClosedTilt(stateObj);
} }
export function canStopTilt(stateObj: CoverEntity): boolean { export function canStopTilt(stateObj: CoverEntity): boolean {

View File

@ -75,6 +75,9 @@ export const DOMAIN_ATTRIBUTES_UNITS = {
vacuum: { vacuum: {
battery_level: "%", battery_level: "%",
}, },
valve: {
current_position: "%",
},
sensor: { sensor: {
battery_level: "%", battery_level: "%",
}, },

View File

@ -16,7 +16,9 @@ export type IntegrationType =
| "helper" | "helper"
| "hub" | "hub"
| "service" | "service"
| "hardware"; | "hardware"
| "entity"
| "system";
export interface IntegrationManifest { export interface IntegrationManifest {
is_built_in: boolean; is_built_in: boolean;

View File

@ -90,7 +90,7 @@ export const enum MediaPlayerEntityFeature {
TURN_ON = 128, TURN_ON = 128,
TURN_OFF = 256, TURN_OFF = 256,
PLAY_MEDIA = 512, PLAY_MEDIA = 512,
VOLUME_BUTTONS = 1024, VOLUME_STEP = 1024,
SELECT_SOURCE = 2048, SELECT_SOURCE = 2048,
STOP = 4096, STOP = 4096,
CLEAR_PLAYLIST = 8192, CLEAR_PLAYLIST = 8192,

View File

@ -18,6 +18,8 @@ export interface TodoItem {
uid: string; uid: string;
summary: string; summary: string;
status: TodoItemStatus; status: TodoItemStatus;
description?: string;
due?: string;
} }
export const enum TodoListEntityFeature { export const enum TodoListEntityFeature {
@ -25,6 +27,9 @@ export const enum TodoListEntityFeature {
DELETE_TODO_ITEM = 2, DELETE_TODO_ITEM = 2,
UPDATE_TODO_ITEM = 4, UPDATE_TODO_ITEM = 4,
MOVE_TODO_ITEM = 8, MOVE_TODO_ITEM = 8,
SET_DUE_DATE_ON_ITEM = 16,
SET_DUE_DATETIME_ON_ITEM = 32,
SET_DESCRIPTION_ON_ITEM = 64,
} }
export const getTodoLists = (hass: HomeAssistant): TodoList[] => export const getTodoLists = (hass: HomeAssistant): TodoList[] =>
@ -74,20 +79,30 @@ export const updateItem = (
hass.callService( hass.callService(
"todo", "todo",
"update_item", "update_item",
{ item: item.uid, rename: item.summary, status: item.status }, {
item: item.uid,
rename: item.summary,
status: item.status,
description: item.description || undefined,
due_datetime: item.due?.includes("T") ? item.due : undefined,
due_date: item.due?.includes("T") ? undefined : item.due || undefined,
},
{ entity_id } { entity_id }
); );
export const createItem = ( export const createItem = (
hass: HomeAssistant, hass: HomeAssistant,
entity_id: string, entity_id: string,
summary: string item: Omit<TodoItem, "uid" | "status">
): Promise<ServiceCallResponse> => ): Promise<ServiceCallResponse> =>
hass.callService( hass.callService(
"todo", "todo",
"add_item", "add_item",
{ {
item: summary, item: item.summary,
description: item.description || undefined,
due_datetime: item.due?.includes("T") ? item.due : undefined,
due_date: item.due?.includes("T") ? undefined : item.due,
}, },
{ entity_id } { entity_id }
); );

View File

@ -4,13 +4,16 @@ import {
mdiClockOutline, mdiClockOutline,
mdiCodeBraces, mdiCodeBraces,
mdiDevices, mdiDevices,
mdiDotsHorizontal,
mdiGestureDoubleTap, mdiGestureDoubleTap,
mdiMapClock,
mdiMapMarker, mdiMapMarker,
mdiMapMarkerRadius, mdiMapMarkerRadius,
mdiMessageAlert, mdiMessageAlert,
mdiMicrophoneMessage, mdiMicrophoneMessage,
mdiNfcVariant, mdiNfcVariant,
mdiNumeric, mdiNumeric,
mdiShape,
mdiStateMachine, mdiStateMachine,
mdiSwapHorizontal, mdiSwapHorizontal,
mdiWeatherSunny, mdiWeatherSunny,
@ -18,8 +21,9 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg"; import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import { AutomationElementGroup } from "./automation";
export const TRIGGER_TYPES = { export const TRIGGER_ICONS = {
calendar: mdiCalendar, calendar: mdiCalendar,
device: mdiDevices, device: mdiDevices,
event: mdiGestureDoubleTap, event: mdiGestureDoubleTap,
@ -38,3 +42,26 @@ export const TRIGGER_TYPES = {
persistent_notification: mdiMessageAlert, persistent_notification: mdiMessageAlert,
zone: mdiMapMarkerRadius, zone: mdiMapMarkerRadius,
}; };
export const TRIGGER_GROUPS: AutomationElementGroup = {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { calendar: {}, sun: {}, time: {}, time_pattern: {}, zone: {} },
},
other: {
icon: mdiDotsHorizontal,
members: {
event: {},
geo_location: {},
homeassistant: {},
mqtt: {},
conversation: {},
tag: {},
template: {},
webhook: {},
persistent_notification: {},
},
},
} as const;

View File

@ -13,11 +13,13 @@ import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { showToast } from "../util/toast"; import { showToast } from "../util/toast";
export const UPDATE_SUPPORT_INSTALL = 1; export enum UpdateEntityFeature {
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2; INSTALL = 1,
export const UPDATE_SUPPORT_PROGRESS = 4; SPECIFIC_VERSION = 2,
export const UPDATE_SUPPORT_BACKUP = 8; PROGRESS = 4,
export const UPDATE_SUPPORT_RELEASE_NOTES = 16; BACKUP = 8,
RELEASE_NOTES = 16,
}
interface UpdateEntityAttributes extends HassEntityAttributeBase { interface UpdateEntityAttributes extends HassEntityAttributeBase {
auto_update: boolean | null; auto_update: boolean | null;
@ -35,7 +37,7 @@ export interface UpdateEntity extends HassEntityBase {
} }
export const updateUsesProgress = (entity: UpdateEntity): boolean => export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) && supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
typeof entity.attributes.in_progress === "number"; typeof entity.attributes.in_progress === "number";
export const updateCanInstall = ( export const updateCanInstall = (
@ -44,7 +46,7 @@ export const updateCanInstall = (
): boolean => ): boolean =>
(entity.state === BINARY_STATE_ON || (entity.state === BINARY_STATE_ON ||
(showSkipped && Boolean(entity.attributes.skipped_version))) && (showSkipped && Boolean(entity.attributes.skipped_version))) &&
supportsFeature(entity, UPDATE_SUPPORT_INSTALL); supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const updateIsInstalling = (entity: UpdateEntity): boolean => export const updateIsInstalling = (entity: UpdateEntity): boolean =>
updateUsesProgress(entity) || !!entity.attributes.in_progress; updateUsesProgress(entity) || !!entity.attributes.in_progress;
@ -176,7 +178,7 @@ export const computeUpdateStateDisplay = (
if (state === "on") { if (state === "on") {
if (updateIsInstalling(stateObj)) { if (updateIsInstalling(stateObj)) {
const supportsProgress = const supportsProgress =
supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS) && supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
typeof attributes.in_progress === "number"; typeof attributes.in_progress === "number";
if (supportsProgress) { if (supportsProgress) {
return hass.localize("ui.card.update.installing_with_progress", { return hass.localize("ui.card.update.installing_with_progress", {

85
src/data/valve.ts Normal file
View File

@ -0,0 +1,85 @@
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { UNAVAILABLE } from "./entity";
import { stateActive } from "../common/entity/state_active";
import { HomeAssistant } from "../types";
export const enum ValveEntityFeature {
OPEN = 1,
CLOSE = 2,
SET_POSITION = 4,
STOP = 8,
}
export function isFullyOpen(stateObj: ValveEntity) {
if (stateObj.attributes.current_position !== undefined) {
return stateObj.attributes.current_position === 100;
}
return stateObj.state === "open";
}
export function isFullyClosed(stateObj: ValveEntity) {
if (stateObj.attributes.current_position !== undefined) {
return stateObj.attributes.current_position === 0;
}
return stateObj.state === "closed";
}
export function isOpening(stateObj: ValveEntity) {
return stateObj.state === "opening";
}
export function isClosing(stateObj: ValveEntity) {
return stateObj.state === "closing";
}
export function canOpen(stateObj: ValveEntity) {
if (stateObj.state === UNAVAILABLE) {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return assumedState || (!isFullyOpen(stateObj) && !isOpening(stateObj));
}
export function canClose(stateObj: ValveEntity): boolean {
if (stateObj.state === UNAVAILABLE) {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return assumedState || (!isFullyClosed(stateObj) && !isClosing(stateObj));
}
export function canStop(stateObj: ValveEntity): boolean {
return stateObj.state !== UNAVAILABLE;
}
interface ValveEntityAttributes extends HassEntityAttributeBase {
current_position?: number;
position?: number;
}
export interface ValveEntity extends HassEntityBase {
attributes: ValveEntityAttributes;
}
export function computeValvePositionStateDisplay(
stateObj: ValveEntity,
hass: HomeAssistant,
position?: number
) {
const statePosition = stateActive(stateObj)
? stateObj.attributes.current_position
: undefined;
const currentPosition = position ?? statePosition;
return currentPosition && currentPosition !== 100
? hass.formatEntityAttributeValue(
stateObj,
"current_position",
Math.round(currentPosition)
)
: "";
}

View File

@ -383,11 +383,13 @@ export const removeMembersFromGroup = (
export const addGroup = ( export const addGroup = (
hass: HomeAssistant, hass: HomeAssistant,
groupName: string, groupName: string,
groupId?: number,
membersToAdd?: ZHAGroupMember[] membersToAdd?: ZHAGroupMember[]
): Promise<ZHAGroup> => ): Promise<ZHAGroup> =>
hass.callWS({ hass.callWS({
type: "zha/group/add", type: "zha/group/add",
group_name: groupName, group_name: groupName,
group_id: groupId,
members: membersToAdd, members: membersToAdd,
}); });

View File

@ -27,6 +27,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"lock", "lock",
"siren", "siren",
"switch", "switch",
"valve",
"water_heater", "water_heater",
]; ];
/** Domains with separate more info dialog. */ /** Domains with separate more info dialog. */
@ -61,6 +62,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"timer", "timer",
"update", "update",
"vacuum", "vacuum",
"valve",
"water_heater", "water_heater",
"weather", "weather",
]; ];

View File

@ -42,7 +42,9 @@ class MoreInfoCover extends LitElement {
protected willUpdate(changedProps: PropertyValues): void { protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (changedProps.has("stateObj") && this.stateObj) { if (changedProps.has("stateObj") && this.stateObj) {
if (!this._mode) { const entityId = this.stateObj.entity_id;
const oldEntityId = changedProps.get("stateObj")?.entity_id;
if (!this._mode || entityId !== oldEntityId) {
this._mode = this._mode =
supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION) || supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION) ||
supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION) supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION)

View File

@ -81,7 +81,7 @@ class MoreInfoMediaPlayer extends LitElement {
: ""} : ""}
</div> </div>
${(supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET) || ${(supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET) ||
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_BUTTONS)) && supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP)) &&
stateActive(stateObj) stateActive(stateObj)
? html` ? html`
<div class="volume"> <div class="volume">
@ -104,8 +104,9 @@ class MoreInfoMediaPlayer extends LitElement {
: ""} : ""}
${supportsFeature( ${supportsFeature(
stateObj, stateObj,
MediaPlayerEntityFeature.VOLUME_BUTTONS MediaPlayerEntityFeature.VOLUME_SET
) ) ||
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP)
? html` ? html`
<ha-icon-button <ha-icon-button
action="volume_down" action="volume_down"

View File

@ -13,13 +13,9 @@ import "../../../components/ha-markdown";
import { isUnavailableState } from "../../../data/entity"; import { isUnavailableState } from "../../../data/entity";
import { import {
UpdateEntity, UpdateEntity,
UpdateEntityFeature,
updateIsInstalling, updateIsInstalling,
updateReleaseNotes, updateReleaseNotes,
UPDATE_SUPPORT_BACKUP,
UPDATE_SUPPORT_INSTALL,
UPDATE_SUPPORT_PROGRESS,
UPDATE_SUPPORT_RELEASE_NOTES,
UPDATE_SUPPORT_SPECIFIC_VERSION,
} from "../../../data/update"; } from "../../../data/update";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
@ -49,7 +45,7 @@ class MoreInfoUpdate extends LitElement {
return html` return html`
${this.stateObj.attributes.in_progress ${this.stateObj.attributes.in_progress
? supportsFeature(this.stateObj, UPDATE_SUPPORT_PROGRESS) && ? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
typeof this.stateObj.attributes.in_progress === "number" typeof this.stateObj.attributes.in_progress === "number"
? html`<mwc-linear-progress ? html`<mwc-linear-progress
.progress=${this.stateObj.attributes.in_progress / 100} .progress=${this.stateObj.attributes.in_progress / 100}
@ -101,7 +97,7 @@ class MoreInfoUpdate extends LitElement {
</div> </div>
</div>` </div>`
: ""} : ""}
${supportsFeature(this.stateObj!, UPDATE_SUPPORT_RELEASE_NOTES) && ${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error !this._error
? !this._releaseNotes ? !this._releaseNotes
? html`<div class="flex center"> ? html`<div class="flex center">
@ -117,7 +113,7 @@ class MoreInfoUpdate extends LitElement {
.content=${this.stateObj.attributes.release_summary} .content=${this.stateObj.attributes.release_summary}
></ha-markdown>` ></ha-markdown>`
: ""} : ""}
${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP) ${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
? html`<hr /> ? html`<hr />
<ha-formfield <ha-formfield
.label=${this.hass.localize( .label=${this.hass.localize(
@ -155,7 +151,7 @@ class MoreInfoUpdate extends LitElement {
)} )}
</mwc-button> </mwc-button>
`} `}
${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL) ${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
? html` ? html`
<mwc-button <mwc-button
@click=${this._handleInstall} @click=${this._handleInstall}
@ -174,7 +170,7 @@ class MoreInfoUpdate extends LitElement {
} }
protected firstUpdated(): void { protected firstUpdated(): void {
if (supportsFeature(this.stateObj!, UPDATE_SUPPORT_RELEASE_NOTES)) { if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
updateReleaseNotes(this.hass, this.stateObj!.entity_id) updateReleaseNotes(this.hass, this.stateObj!.entity_id)
.then((result) => { .then((result) => {
this._releaseNotes = result; this._releaseNotes = result;
@ -186,7 +182,7 @@ class MoreInfoUpdate extends LitElement {
} }
get _shouldCreateBackup(): boolean | null { get _shouldCreateBackup(): boolean | null {
if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) { if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return null; return null;
} }
const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
@ -206,7 +202,7 @@ class MoreInfoUpdate extends LitElement {
} }
if ( if (
supportsFeature(this.stateObj!, UPDATE_SUPPORT_SPECIFIC_VERSION) && supportsFeature(this.stateObj!, UpdateEntityFeature.SPECIFIC_VERSION) &&
this.stateObj!.attributes.latest_version this.stateObj!.attributes.latest_version
) { ) {
installData.version = this.stateObj!.attributes.latest_version; installData.version = this.stateObj!.attributes.latest_version;

View File

@ -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`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateOverride=${this._stateOverride}
></ha-more-info-state-header>
<div class="controls">
<div class="main-control">
${
this._mode === "position"
? html`
${supportsPosition
? html`
<ha-state-control-valve-position
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-position>
`
: nothing}
`
: nothing
}
${
this._mode === "button"
? html`
${supportsOpenCloseWithoutStop
? html`
<ha-state-control-valve-toggle
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-toggle>
`
: supportsOpenClose
? html`
<ha-state-control-valve-buttons
.stateObj=${this.stateObj}
.hass=${this.hass}
></ha-state-control-valve-buttons>
`
: nothing}
`
: nothing
}
</div>
${
supportsPosition && supportsOpenClose
? html`
<ha-icon-button-group>
<ha-icon-button-toggle
.label=${this.hass.localize(
`ui.dialogs.more_info_control.valve.switch_mode.position`
)}
.selected=${this._mode === "position"}
.path=${mdiMenu}
.mode=${"position"}
@click=${this._setMode}
></ha-icon-button-toggle>
<ha-icon-button-toggle
.label=${this.hass.localize(
`ui.dialogs.more_info_control.valve.switch_mode.button`
)}
.selected=${this._mode === "button"}
.path=${mdiSwapVertical}
.mode=${"button"}
@click=${this._setMode}
></ha-icon-button-toggle>
</ha-icon-button-group>
`
: nothing
}
</div>
</div>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="current_position,current_tilt_position"
></ha-attributes>
`;
}
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;
}
}

View File

@ -35,6 +35,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = {
timer: () => import("./controls/more-info-timer"), timer: () => import("./controls/more-info-timer"),
update: () => import("./controls/more-info-update"), update: () => import("./controls/more-info-update"),
vacuum: () => import("./controls/more-info-vacuum"), vacuum: () => import("./controls/more-info-vacuum"),
valve: () => import("./controls/more-info-valve"),
water_heater: () => import("./controls/more-info-water_heater"), water_heater: () => import("./controls/more-info-water_heater"),
weather: () => import("./controls/more-info-weather"), weather: () => import("./controls/more-info-weather"),
}; };

View File

@ -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<void> {
this._params = params;
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this._cancel}
defaultAction="ignore"
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.update_backup.title")
)}
>
<p>${this.hass.localize("ui.dialogs.update_backup.text")}</p>
<ha-button @click=${this._no} slot="secondaryAction">
${this.hass!.localize("ui.common.no")}
</ha-button>
<ha-button @click=${this._yes} slot="primaryAction">
${this.hass.localize("ui.dialogs.update_backup.create")}
</ha-button>
</ha-dialog>
`;
}
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;
}
}

View File

@ -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<boolean | null>((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);
}
},
},
});
});

View File

@ -668,7 +668,12 @@ export class HaVoiceCommandDialog extends LitElement {
ha-button-menu { ha-button-menu {
--mdc-theme-on-primary: var(--text-primary-color); --mdc-theme-on-primary: var(--text-primary-color);
--mdc-theme-primary: var(--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 { ha-button-menu ha-button {
--mdc-theme-primary: var(--secondary-text-color); --mdc-theme-primary: var(--secondary-text-color);
@ -689,7 +694,7 @@ export class HaVoiceCommandDialog extends LitElement {
height: 28px; height: 28px;
margin-left: 4px; margin-left: 4px;
margin-inline-start: 4px; margin-inline-start: 4px;
margin-inline-end: 4px; margin-inline-end: initial;
direction: var(--direction); direction: var(--direction);
} }
ha-list-item { ha-list-item {
@ -698,7 +703,7 @@ export class HaVoiceCommandDialog extends LitElement {
ha-list-item ha-svg-icon { ha-list-item ha-svg-icon {
margin-left: 4px; margin-left: 4px;
margin-inline-start: 4px; margin-inline-start: 4px;
margin-inline-end: 4px; margin-inline-end: initial;
direction: var(--direction); direction: var(--direction);
display: block; display: block;
} }

View File

@ -124,6 +124,12 @@ export class HaTabsSubpageDataTable extends LitElement {
*/ */
@property({ type: String }) public noDataText?: string; @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; @property() public route!: Route;
/** /**
@ -198,7 +204,11 @@ export class HaTabsSubpageDataTable extends LitElement {
.mainPage=${this.mainPage} .mainPage=${this.mainPage}
.supervisor=${this.supervisor} .supervisor=${this.supervisor}
> >
${!this.hideFilterMenu ${this.empty
? html`<div class="center">
<slot name="empty">${this.noDataText}</slot>
</div>`
: html`${!this.hideFilterMenu
? html` ? html`
<div slot="toolbar-icon"> <div slot="toolbar-icon">
${this.narrow ${this.narrow
@ -229,11 +239,11 @@ export class HaTabsSubpageDataTable extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.columns=${this.columns} .columns=${this.columns}
.data=${this.data} .data=${this.data}
.noDataText=${this.noDataText}
.filter=${this.filter} .filter=${this.filter}
.selectable=${this.selectable} .selectable=${this.selectable}
.hasFab=${this.hasFab} .hasFab=${this.hasFab}
.id=${this.id} .id=${this.id}
.noDataText=${this.noDataText}
.dir=${computeRTLDirection(this.hass)} .dir=${computeRTLDirection(this.hass)}
.clickable=${this.clickable} .clickable=${this.clickable}
.appendRow=${this.appendRow} .appendRow=${this.appendRow}
@ -247,7 +257,8 @@ export class HaTabsSubpageDataTable extends LitElement {
</div> </div>
` `
: html` <div slot="header"></div> `} : html` <div slot="header"></div> `}
</ha-data-table> </ha-data-table>`}
<div slot="fab"><slot name="fab"></slot></div> <div slot="fab"><slot name="fab"></slot></div>
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;
@ -374,6 +385,16 @@ export class HaTabsSubpageDataTable extends LitElement {
.filter-menu { .filter-menu {
position: relative; position: relative;
} }
.center {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
box-sizing: border-box;
height: 100%;
width: 100%;
padding: 16px;
}
`; `;
} }
} }

View File

@ -1,8 +1,8 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiCalendarClock, mdiClose } from "@mdi/js"; import { mdiCalendarClock } from "@mdi/js";
import { toDate } from "date-fns-tz"; import { toDate } from "date-fns-tz";
import { addDays, isSameDay } from "date-fns/esm"; 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 { property, state } from "lit/decorators";
import { formatDate } from "../../common/datetime/format_date"; import { formatDate } from "../../common/datetime/format_date";
import { formatDateTime } from "../../common/datetime/format_date_time"; import { formatDateTime } from "../../common/datetime/format_date_time";
@ -11,6 +11,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { isDate } from "../../common/string/is_date"; import { isDate } from "../../common/string/is_date";
import "../../components/entity/state-info"; import "../../components/entity/state-info";
import "../../components/ha-date-input"; import "../../components/ha-date-input";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-time-input"; import "../../components/ha-time-input";
import { import {
CalendarEventMutableParams, CalendarEventMutableParams,
@ -65,15 +66,7 @@ class DialogCalendarEventDetail extends LitElement {
@closed=${this.closeDialog} @closed=${this.closeDialog}
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
.heading=${html` .heading=${createCloseHeading(this.hass, this._data!.summary)}
<div class="header_title">${this._data!.summary}</div>
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
dialogAction="close"
class="header_button"
></ha-icon-button>
`}
> >
<div class="content"> <div class="content">
${this._error ${this._error

View File

@ -1,5 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiClose } from "@mdi/js";
import { formatInTimeZone, toDate } from "date-fns-tz"; import { formatInTimeZone, toDate } from "date-fns-tz";
import { import {
addDays, addDays,
@ -9,7 +8,7 @@ import {
startOfHour, startOfHour,
} from "date-fns/esm"; } from "date-fns/esm";
import { HassEntity } from "home-assistant-js-websocket"; 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 { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@ -18,23 +17,24 @@ import { supportsFeature } from "../../common/entity/supports-feature";
import { isDate } from "../../common/string/is_date"; import { isDate } from "../../common/string/is_date";
import "../../components/entity/ha-entity-picker"; import "../../components/entity/ha-entity-picker";
import "../../components/ha-date-input"; import "../../components/ha-date-input";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-textarea"; import "../../components/ha-textarea";
import "../../components/ha-time-input"; import "../../components/ha-time-input";
import { import {
CalendarEntityFeature, CalendarEntityFeature,
CalendarEventMutableParams, CalendarEventMutableParams,
RecurrenceRange,
createCalendarEvent, createCalendarEvent,
deleteCalendarEvent, deleteCalendarEvent,
RecurrenceRange,
updateCalendarEvent, updateCalendarEvent,
} from "../../data/calendar"; } from "../../data/calendar";
import { TimeZone } from "../../data/translation";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../lovelace/components/hui-generic-entity-row"; import "../lovelace/components/hui-generic-entity-row";
import "./ha-recurrence-rule-editor"; import "./ha-recurrence-rule-editor";
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box"; import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
import { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor"; import { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor";
import { TimeZone } from "../../data/translation";
const CALENDAR_DOMAINS = ["calendar"]; const CALENDAR_DOMAINS = ["calendar"];
@ -142,19 +142,12 @@ class DialogCalendarEventEditor extends LitElement {
@closed=${this.closeDialog} @closed=${this.closeDialog}
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
.heading=${html` .heading=${createCloseHeading(
<div class="header_title"> this.hass,
${isCreate isCreate
? this.hass.localize("ui.components.calendar.event.add") ? this.hass.localize("ui.components.calendar.event.add")
: this._summary} : this._summary
</div> )}
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
dialogAction="close"
class="header_button"
></ha-icon-button>
`}
> >
<div class="content"> <div class="content">
${this._error ${this._error
@ -584,10 +577,12 @@ class DialogCalendarEventEditor extends LitElement {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
@media all and (min-width: 450px and min-height: 500px) {
ha-dialog { ha-dialog {
--mdc-dialog-min-width: min(600px, 95vw); --mdc-dialog-min-width: min(600px, 95vw);
--mdc-dialog-max-width: min(600px, 95vw); --mdc-dialog-max-width: min(600px, 95vw);
} }
}
state-info { state-info {
line-height: 40px; line-height: 40px;
} }

View File

@ -37,7 +37,7 @@ import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; 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 { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context"; import { fullEntitiesContext } from "../../../../data/context";
@ -82,9 +82,9 @@ export const getType = (action: Action | undefined) => {
if (["and", "or", "not"].some((key) => key in action)) { if (["and", "or", "not"].some((key) => key in action)) {
return "condition" as const; return "condition" as const;
} }
return Object.keys(ACTION_TYPES).find( return Object.keys(ACTION_ICONS).find(
(option) => option in action (option) => option in action
) as keyof typeof ACTION_TYPES; ) as keyof typeof ACTION_ICONS;
}; };
export interface ActionElement extends LitElement { export interface ActionElement extends LitElement {
@ -190,7 +190,7 @@ export default class HaAutomationActionRow extends LitElement {
<h3 slot="header"> <h3 slot="header">
<ha-svg-icon <ha-svg-icon
class="action-icon" class="action-icon"
.path=${ACTION_TYPES[type!]} .path=${ACTION_ICONS[type!]}
></ha-svg-icon> ></ha-svg-icon>
${capitalizeFirstLetter( ${capitalizeFirstLetter(
describeAction(this.hass, this._entityReg, this.action) describeAction(this.hass, this._entityReg, this.action)

View File

@ -1,57 +1,26 @@
import "@material/mwc-button"; import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list"; import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentPaste,
mdiDrag,
mdiPlus,
} from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs"; import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; 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";
import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { ACTION_TYPES } from "../../../../data/action"; import { getService, isService } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation"; import type { AutomationClipboard } from "../../../../data/automation";
import { Action } from "../../../../data/script"; import { Action } from "../../../../data/script";
import { sortableStyles } from "../../../../resources/ha-sortable-style"; import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable"; 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 type HaAutomationActionRow from "./ha-automation-action-row";
import { getType } 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") @customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement { export default class HaAutomationAction extends LitElement {
@ -150,42 +119,27 @@ export default class HaAutomationAction extends LitElement {
` `
)} )}
</div> </div>
<ha-button-menu <div class="buttons">
@action=${this._addAction}
.disabled=${this.disabled}
fixed
>
<ha-button <ha-button
slot="trigger"
outlined outlined
.disabled=${this.disabled} .disabled=${this.disabled}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.add" "ui.panel.config.automation.editor.actions.add"
)} )}
@click=${this._addActionDialog}
> >
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon> <ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button> </ha-button>
${this._clipboard?.action <ha-button
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon"> .disabled=${this.disabled}
${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.paste" "ui.panel.config.automation.editor.actions.add_building_block"
)} )}
(${this.hass.localize( @click=${this._addActionBuildingBlockDialog}
`ui.panel.config.automation.editor.actions.type.${ >
getType(this._clipboard.action) || "unknown" <ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
}.label` </ha-button>
)}) </div>
<ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon
></mwc-list-item>`
: nothing}
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`
)}
</ha-button-menu>
`; `;
} }
@ -213,6 +167,43 @@ 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),
});
} 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) { private async _enterReOrderMode(ev: CustomEvent) {
if (this.nested) return; if (this.nested) return;
ev.stopPropagation(); ev.stopPropagation();
@ -258,25 +249,6 @@ export default class HaAutomationAction extends LitElement {
return this._actionKeys.get(action)!; return this._actionKeys.get(action)!;
} }
private _addAction(ev: CustomEvent<ActionDetail>) {
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) { private _moveUp(ev) {
const index = (ev.target as any).index; const index = (ev.target as any).index;
const newIndex = index - 1; const newIndex = index - 1;
@ -328,22 +300,6 @@ export default class HaAutomationAction extends LitElement {
}); });
} }
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(ACTION_TYPES) as Entries<typeof ACTION_TYPES>)
.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 { static get styles(): CSSResultGroup {
return [ return [
sortableStyles, sortableStyles,
@ -370,6 +326,11 @@ export default class HaAutomationAction extends LitElement {
pointer-events: none; pointer-events: none;
height: 24px; height: 24px;
} }
.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
`, `,
]; ];
} }

View File

@ -7,7 +7,7 @@ import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-select"; import "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select"; import type { HaSelect } from "../../../../../components/ha-select";
import type { Condition } from "../../../../../data/automation"; import type { Condition } from "../../../../../data/automation";
import { CONDITION_TYPES } from "../../../../../data/condition"; import { CONDITION_ICONS } from "../../../../../data/condition";
import { Entries, HomeAssistant } from "../../../../../types"; import { Entries, HomeAssistant } from "../../../../../types";
import "../../condition/ha-automation-condition-editor"; import "../../condition/ha-automation-condition-editor";
import type { ActionElement } from "../ha-automation-action-row"; import type { ActionElement } from "../ha-automation-action-row";
@ -55,7 +55,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
private _processedTypes = memoizeOne( private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] => (localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>) (Object.entries(CONDITION_ICONS) as Entries<typeof CONDITION_ICONS>)
.map( .map(
([condition, icon]) => ([condition, icon]) =>
[ [

View File

@ -0,0 +1,579 @@
import "@material/mwc-list/mwc-list";
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
import Fuse, { IFuseOptions } from "fuse.js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { domainIcon } from "../../../common/entity/domain_icon";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import { stringCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-dialog";
import type { HaDialog } from "../../../components/ha-dialog";
import "../../../components/ha-dialog-header";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
import "../../../components/ha-list-item";
import "../../../components/search-input";
import {
ACTION_GROUPS,
ACTION_ICONS,
SERVICE_PREFIX,
getService,
isService,
} from "../../../data/action";
import { AutomationElementGroup } from "../../../data/automation";
import { CONDITION_GROUPS, CONDITION_ICONS } from "../../../data/condition";
import {
IntegrationManifest,
domainToName,
fetchIntegrationManifests,
} from "../../../data/integration";
import { TRIGGER_GROUPS, TRIGGER_ICONS } from "../../../data/trigger";
import { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import {
AddAutomationElementDialogParams,
PASTE_VALUE,
} from "./show-add-automation-element-dialog";
const TYPES = {
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
condition: {
groups: CONDITION_GROUPS,
icons: CONDITION_ICONS,
},
action: {
groups: ACTION_GROUPS,
icons: ACTION_ICONS,
},
};
interface ListItem {
key: string;
name: string;
description: string;
icon: string;
group: boolean;
}
interface DomainManifestLookup {
[domain: string]: IntegrationManifest;
}
const ENTITY_DOMAINS_OTHER = new Set([
"date",
"datetime",
"device_tracker",
"text",
"time",
"tts",
"update",
"weather",
"image_processing",
]);
@customElement("add-automation-element-dialog")
class DialogAddAutomationElement extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: AddAutomationElementDialogParams;
@state() private _group?: string;
@state() private _prev?: string;
@state() private _filter = "";
@state() private _manifests?: DomainManifestLookup;
@query("ha-dialog") private _dialog?: HaDialog;
private _fullScreen = false;
private _width?: number;
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;
}
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"],
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;
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<ListItem> = {
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"],
group: string | 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_media_player") {
result.unshift(this._convertToItem("play_media", {}, type, localize));
}
return result;
}
const groups: AutomationElementGroup = group
? TYPES[type].groups[group].members!
: TYPES[type].groups;
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, undefined)
);
} else if (this._group === "helpers") {
result.unshift(
...this._serviceGroups(localize, services, manifests, "helper")
);
} else if (this._group === "other") {
result.unshift(
...this._serviceGroups(localize, services, manifests, "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 = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests: DomainManifestLookup | undefined,
type: "helper" | "other" | undefined
): ListItem[] => {
if (!services || !manifests) {
return [];
}
const result: ListItem[] = [];
Object.keys(services)
.sort()
.forEach((domain) => {
const manifest = manifests[domain];
if (
(type === undefined &&
manifest?.integration_type === "entity" &&
!ENTITY_DOMAINS_OTHER.has(domain)) ||
(type === "helper" && manifest?.integration_type === "helper") ||
(type === "other" &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
!["helper", "entity"].includes(
manifest?.integration_type || ""
)))
) {
result.push({
group: true,
icon: domainIcon(domain),
key: `${SERVICE_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
});
}
});
return result;
}
);
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) {
result.push({
group: false,
icon: domainIcon(dmn),
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 render() {
if (!this._params) {
return nothing;
}
const items = this._filter
? this._getFilteredItems(
this._params.type,
this._group,
this._filter,
this.hass.localize,
this.hass.services,
this._manifests
)
: this._getGroupItems(
this._params.type,
this._group,
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`
<ha-dialog
open
hideActions
@opened=${this._opened}
@closed=${this.closeDialog}
.heading=${true}
>
<div slot="heading">
<ha-dialog-header>
<span slot="title"
>${this._group
? groupName
: this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.add`
)}</span
>
${this._group && this._group !== this._params.group
? html`<ha-icon-button-prev
slot="navigationIcon"
@click=${this._back}
></ha-icon-button-prev>`
: html`<ha-icon-button
.path=${mdiClose}
slot="navigationIcon"
dialogAction="cancel"
></ha-icon-button>`}
</ha-dialog-header>
<search-input
dialogInitialFocus=${ifDefined(this._fullScreen ? undefined : "")}
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._filterChanged}
.label=${groupName
? this.hass.localize(
"ui.panel.config.automation.editor.search_in",
{ group: groupName }
)
: this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.search`
)}
></search-input>
</div>
<mwc-list
dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)}
innerRole="listbox"
itemRoles="option"
rootTabbable
style=${styleMap({
width: `${this._width}px`,
height: `${this._height}px`,
})}
>
${this._params.clipboardItem &&
!this._filter &&
(!this._group ||
items.find((item) => item.key === this._params!.clipboardItem))
? html`<ha-list-item
twoline
class="paste"
.value=${PASTE_VALUE}
graphic="icon"
hasMeta
@request-selected=${this._selected}
>
${this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.paste`
)}
<span slot="secondary"
>${this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
)}</span
>
<ha-svg-icon
slot="graphic"
.path=${mdiContentPaste}
></ha-svg-icon
><ha-svg-icon slot="meta" .path=${mdiPlus}></ha-svg-icon>
</ha-list-item>
<li divider role="separator"></li>`
: ""}
${repeat(
items,
(item) => item.key,
(item) => html`
<ha-list-item
.twoline=${Boolean(item.description)}
.value=${item.key}
.group=${item.group}
graphic="icon"
hasMeta
@request-selected=${this._selected}
>
${item.name}
<span slot="secondary">${item.description}</span>
<ha-svg-icon slot="graphic" .path=${item.icon}></ha-svg-icon>
${item.group
? html`<ha-icon-next slot="meta"></ha-icon-next>`
: html`<ha-svg-icon
slot="meta"
.path=${mdiPlus}
></ha-svg-icon>`}
</ha-list-item>
`
)}
</mwc-list>
</ha-dialog>
`;
}
private _back() {
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;
}
search-input {
display: block;
margin: 0 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"add-automation-element-dialog": DialogAddAutomationElement;
}
}

View File

@ -29,7 +29,7 @@ import "../../../../components/ha-icon-button";
import type { AutomationClipboard } from "../../../../data/automation"; import type { AutomationClipboard } from "../../../../data/automation";
import { Condition, testCondition } from "../../../../data/automation"; import { Condition, testCondition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n"; import { describeCondition } from "../../../../data/automation_i18n";
import { CONDITION_TYPES } from "../../../../data/condition"; import { CONDITION_ICONS } from "../../../../data/condition";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context"; import { fullEntitiesContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../../data/entity_registry";
@ -123,7 +123,7 @@ export default class HaAutomationConditionRow extends LitElement {
<h3 slot="header"> <h3 slot="header">
<ha-svg-icon <ha-svg-icon
class="condition-icon" class="condition-icon"
.path=${CONDITION_TYPES[this.condition.condition]} .path=${CONDITION_ICONS[this.condition.condition]}
></ha-svg-icon> ></ha-svg-icon>
${capitalizeFirstLetter( ${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg) describeCondition(this.condition, this.hass, this._entityReg)

View File

@ -1,25 +1,18 @@
import "@material/mwc-button"; import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list"; import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentPaste,
mdiDrag,
mdiPlus,
} from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
nothing,
PropertyValues, PropertyValues,
css,
html,
nothing,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs"; import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
@ -28,30 +21,15 @@ import type {
AutomationClipboard, AutomationClipboard,
Condition, Condition,
} from "../../../../data/automation"; } 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 { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable"; import type { SortableInstance } from "../../../../resources/sortable";
import "./types/ha-automation-condition-and"; import type { HomeAssistant } from "../../../../types";
import "./types/ha-automation-condition-device"; import {
import "./types/ha-automation-condition-numeric_state"; PASTE_VALUE,
import "./types/ha-automation-condition-state"; showAddAutomationElementDialog,
import "./types/ha-automation-condition-sun"; } from "../show-add-automation-element-dialog";
import "./types/ha-automation-condition-template"; import "./ha-automation-condition-row";
import "./types/ha-automation-condition-time"; import type HaAutomationConditionRow from "./ha-automation-condition-row";
import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
const PASTE_VALUE = "__paste__";
@customElement("ha-automation-condition") @customElement("ha-automation-condition")
export default class HaAutomationCondition extends LitElement { export default class HaAutomationCondition extends LitElement {
@ -197,43 +175,69 @@ export default class HaAutomationCondition extends LitElement {
` `
)} )}
</div> </div>
<ha-button-menu <div class="buttons">
@action=${this._addCondition}
.disabled=${this.disabled}
fixed
>
<ha-button <ha-button
slot="trigger"
outlined outlined
.disabled=${this.disabled} .disabled=${this.disabled}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.add" "ui.panel.config.automation.editor.conditions.add"
)} )}
@click=${this._addConditionDialog}
> >
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon> <ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button> </ha-button>
${this._clipboard?.condition <ha-button
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon"> .disabled=${this.disabled}
${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.paste" "ui.panel.config.automation.editor.conditions.add_building_block"
)} )}
(${this.hass.localize( @click=${this._addConditionBuildingBlockDialog}
`ui.panel.config.automation.editor.conditions.type.${this._clipboard.condition.condition}.label` >
)}) <ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
<ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon </ha-button>
></mwc-list-item>` </div>
: nothing}
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`
)}
</ha-button-menu>
`; `;
} }
private _addConditionDialog() {
showAddAutomationElementDialog(this, {
type: "condition",
add: this._addCondition,
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<Condition, "condition">;
};
conditions = this.conditions.concat({
condition: condition as any,
...elClass.defaultConfig,
});
}
this._focusLastConditionOnChange = true;
fireEvent(this, "value-changed", { value: conditions });
};
private async _enterReOrderMode(ev: CustomEvent) { private async _enterReOrderMode(ev: CustomEvent) {
if (this.nested) return; if (this.nested) return;
ev.stopPropagation(); ev.stopPropagation();
@ -282,32 +286,6 @@ export default class HaAutomationCondition extends LitElement {
return this._conditionKeys.get(condition)!; return this._conditionKeys.get(condition)!;
} }
private _addCondition(ev: CustomEvent<ActionDetail>) {
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<Condition, "condition">;
};
conditions = this.conditions.concat({
condition: condition as any,
...elClass.defaultConfig,
});
}
this._focusLastConditionOnChange = true;
fireEvent(this, "value-changed", { value: conditions });
}
private _moveUp(ev) { private _moveUp(ev) {
const index = (ev.target as any).index; const index = (ev.target as any).index;
const newIndex = index - 1; const newIndex = index - 1;
@ -361,22 +339,6 @@ export default class HaAutomationCondition extends LitElement {
}); });
} }
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>)
.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 { static get styles(): CSSResultGroup {
return [ return [
sortableStyles, sortableStyles,
@ -403,6 +365,11 @@ export default class HaAutomationCondition extends LitElement {
pointer-events: none; pointer-events: none;
height: 24px; height: 24px;
} }
.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
`, `,
]; ];
} }

View File

@ -486,7 +486,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
value.valid value.valid
? "" ? ""
: html`${this.hass.localize( : html`${this.hass.localize(
`ui.panel.config.automation.editor.${key}s.header` `ui.panel.config.automation.editor.${key}s.name`
)}: )}:
${value.error}<br />` ${value.error}<br />`
); );

View File

@ -7,11 +7,19 @@ import {
mdiPlay, mdiPlay,
mdiPlayCircleOutline, mdiPlayCircleOutline,
mdiPlus, mdiPlus,
mdiRobotHappy,
mdiStopCircleOutline, mdiStopCircleOutline,
mdiTransitConnection, mdiTransitConnection,
} from "@mdi/js"; } from "@mdi/js";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; 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 { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { differenceInDays } from "date-fns/esm"; import { differenceInDays } from "date-fns/esm";
@ -295,6 +303,7 @@ class HaAutomationPicker extends LitElement {
.activeFilters=${this._activeFilters} .activeFilters=${this._activeFilters}
.columns=${this._columns(this.narrow, this.hass.locale)} .columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._automations(this.automations, this._filteredAutomations)} .data=${this._automations(this.automations, this._filteredAutomations)}
.empty=${!this.automations.length}
@row-click=${this._handleRowClicked} @row-click=${this._handleRowClicked}
.noDataText=${this.hass.localize( .noDataText=${this.hass.localize(
"ui.panel.config.automation.picker.no_automations" "ui.panel.config.automation.picker.no_automations"
@ -318,6 +327,35 @@ class HaAutomationPicker extends LitElement {
@related-changed=${this._relatedFilterChanged} @related-changed=${this._relatedFilterChanged}
> >
</ha-button-related-filter-menu> </ha-button-related-filter-menu>
${!this.automations.length
? html` <div class="empty" slot="empty">
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
<h1>
${this.hass.localize(
"ui.panel.config.automation.picker.empty_header"
)}
</h1>
<p>
${this.hass.localize(
"ui.panel.config.automation.picker.empty_text_1"
)}<br />
${this.hass.localize(
"ui.panel.config.automation.picker.empty_text_2"
)}
</p>
<a
href=${documentationUrl(this.hass, "/docs/automation/editor/")}
target="_blank"
rel="noreferrer"
>
<ha-button>
${this.hass.localize(
"ui.panel.config.automation.picker.learn_more"
)}
</ha-button>
</a>
</div>`
: nothing}
<ha-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(
@ -475,9 +513,7 @@ class HaAutomationPicker extends LitElement {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
${this.hass.localize( ${this.hass.localize("ui.panel.config.common.learn_more")}
"ui.panel.config.automation.picker.learn_more"
)}
</a> </a>
</p> </p>
`, `,
@ -505,7 +541,16 @@ class HaAutomationPicker extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return haStyle; return [
haStyle,
css`
.empty {
--paper-font-headline_-_font-size: 28px;
--mdc-icon-size: 80px;
max-width: 500px;
}
`,
];
} }
} }

View File

@ -7,7 +7,14 @@ import {
mdiRayStartArrow, mdiRayStartArrow,
mdiRefresh, mdiRefresh,
} from "@mdi/js"; } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@ -41,6 +48,8 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { computeRTL } from "../../../common/util/compute_rtl"; import { computeRTL } from "../../../common/util/compute_rtl";
const TABS = ["details", "automation_config", "timeline", "logbook"] as const;
@customElement("ha-automation-trace") @customElement("ha-automation-trace")
export class HaAutomationTrace extends LitElement { export class HaAutomationTrace extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -67,12 +76,7 @@ export class HaAutomationTrace extends LitElement {
@state() private _logbookEntries?: LogbookEntry[]; @state() private _logbookEntries?: LogbookEntry[];
@state() private _view: @state() private _view: (typeof TABS)[number] | "blueprint" = "details";
| "details"
| "config"
| "timeline"
| "logbook"
| "blueprint" = "details";
@query("hat-script-graph") private _graph?: HatScriptGraph; @query("hat-script-graph") private _graph?: HatScriptGraph;
@ -213,9 +217,15 @@ export class HaAutomationTrace extends LitElement {
</div> </div>
${this._traces === undefined ${this._traces === undefined
? html`<div class="container">Loading…</div>` ? html`<div class="container">
${this.hass!.localize("ui.common.loading")}
</div>`
: this._traces.length === 0 : this._traces.length === 0
? html`<div class="container">No traces found</div>` ? html`<div class="container">
${this.hass!.localize(
"ui.panel.config.automation.trace.no_traces_found"
)}
</div>`
: this._trace === undefined : this._trace === undefined
? "" ? ""
: html` : html`
@ -230,20 +240,17 @@ export class HaAutomationTrace extends LitElement {
<div class="info"> <div class="info">
<div class="tabs top"> <div class="tabs top">
${[ ${TABS.map(
["details", "Step Details"], (view) => html`
["timeline", "Trace Timeline"],
["logbook", "Related logbook entries"],
["config", "Automation Config"],
].map(
([view, label]) => html`
<button <button
tabindex="0" tabindex="0"
.view=${view} .view=${view}
class=${classMap({ active: this._view === view })} class=${classMap({ active: this._view === view })}
@click=${this._showTab} @click=${this._showTab}
> >
${label} ${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.${view}`
)}
</button> </button>
` `
)} )}
@ -257,7 +264,9 @@ export class HaAutomationTrace extends LitElement {
})} })}
@click=${this._showTab} @click=${this._showTab}
> >
Blueprint Config ${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.blueprint_config`
)}
</button> </button>
` `
: ""} : ""}
@ -265,7 +274,7 @@ export class HaAutomationTrace extends LitElement {
${this._selected === undefined || ${this._selected === undefined ||
this._logbookEntries === undefined || this._logbookEntries === undefined ||
trackedNodes === undefined trackedNodes === undefined
? "" ? nothing
: this._view === "details" : this._view === "details"
? html` ? html`
<ha-trace-path-details <ha-trace-path-details
@ -278,7 +287,7 @@ export class HaAutomationTrace extends LitElement {
.renderedNodes=${renderedNodes!} .renderedNodes=${renderedNodes!}
></ha-trace-path-details> ></ha-trace-path-details>
` `
: this._view === "config" : this._view === "automation_config"
? html` ? html`
<ha-trace-config <ha-trace-config
.hass=${this.hass} .hass=${this.hass}

View File

@ -1,11 +1,13 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js"; import { mdiHelpCircle } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../../common/array/ensure-array";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-markdown";
import { import {
Condition, Condition,
ManualAutomationConfig, ManualAutomationConfig,
@ -83,6 +85,14 @@ export class HaManualAutomationEditor extends LitElement {
></ha-icon-button> ></ha-icon-button>
</a> </a>
</div> </div>
${!this.hass.userData?.showAdvanced &&
!ensureArray(this.config.trigger)?.length
? html`<p>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.description"
)}
</p>`
: nothing}
<ha-automation-trigger <ha-automation-trigger
role="region" role="region"
@ -98,6 +108,9 @@ export class HaManualAutomationEditor extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header" "ui.panel.config.automation.editor.conditions.header"
)} )}
<span class="small"
>(${this.hass.localize("ui.common.optional")})</span
>
</h2> </h2>
<a <a
href=${documentationUrl(this.hass, "/docs/automation/condition/")} href=${documentationUrl(this.hass, "/docs/automation/condition/")}
@ -112,6 +125,15 @@ export class HaManualAutomationEditor extends LitElement {
></ha-icon-button> ></ha-icon-button>
</a> </a>
</div> </div>
${!this.hass.userData?.showAdvanced &&
!ensureArray(this.config.condition)?.length
? html`<p>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.description",
{ user: this.hass.user?.name }
)}
</p>`
: nothing}
<ha-automation-condition <ha-automation-condition
role="region" role="region"
@ -143,6 +165,14 @@ export class HaManualAutomationEditor extends LitElement {
</a> </a>
</div> </div>
</div> </div>
${!this.hass.userData?.showAdvanced &&
!ensureArray(this.config.action)?.length
? html`<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.description"
)}
</p>`
: nothing}
<ha-automation-action <ha-automation-action
role="region" role="region"
@ -207,9 +237,11 @@ export class HaManualAutomationEditor extends LitElement {
margin: 0; margin: 0;
} }
p { p {
margin-bottom: 0; margin-top: 0;
} }
.header { .header {
margin-top: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
@ -217,13 +249,18 @@ export class HaManualAutomationEditor extends LitElement {
margin-top: -16px; margin-top: -16px;
} }
.header .name { .header .name {
font-size: 20px;
font-weight: 400; font-weight: 400;
flex: 1; flex: 1;
margin-bottom: 16px;
} }
.header a { .header a {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.header .small {
font-size: small;
font-weight: normal;
line-height: 0;
}
`, `,
]; ];
} }

View File

@ -0,0 +1,22 @@
import { fireEvent } from "../../../common/dom/fire_event";
export const PASTE_VALUE = "__paste__";
export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action";
add: (key: string) => void;
clipboardItem: string | undefined;
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,
});
};

View File

@ -37,7 +37,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context"; import { fullEntitiesContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { TRIGGER_TYPES } from "../../../../data/trigger"; import { TRIGGER_ICONS } from "../../../../data/trigger";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@ -150,7 +150,7 @@ export default class HaAutomationTriggerRow extends LitElement {
<h3 slot="header"> <h3 slot="header">
<ha-svg-icon <ha-svg-icon
class="trigger-icon" class="trigger-icon"
.path=${TRIGGER_TYPES[this.trigger.platform]} .path=${TRIGGER_ICONS[this.trigger.platform]}
></ha-svg-icon> ></ha-svg-icon>
${describeTrigger(this.trigger, this.hass, this._entityReg)} ${describeTrigger(this.trigger, this.hass, this._entityReg)}
</h3> </h3>

View File

@ -1,59 +1,25 @@
import "@material/mwc-button"; import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list"; import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentPaste,
mdiDrag,
mdiPlus,
} from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs"; import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; 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";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { AutomationClipboard, Trigger } from "../../../../data/automation"; import { AutomationClipboard, Trigger } from "../../../../data/automation";
import { TRIGGER_TYPES } from "../../../../data/trigger";
import { sortableStyles } from "../../../../resources/ha-sortable-style"; import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable"; import type { SortableInstance } from "../../../../resources/sortable";
import { Entries, HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "./ha-automation-trigger-row"; import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row"; import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import "./types/ha-automation-trigger-calendar"; import {
import "./types/ha-automation-trigger-conversation"; PASTE_VALUE,
import "./types/ha-automation-trigger-device"; showAddAutomationElementDialog,
import "./types/ha-automation-trigger-event"; } from "../show-add-automation-element-dialog";
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__";
@customElement("ha-automation-trigger") @customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends LitElement { export default class HaAutomationTrigger extends LitElement {
@ -147,47 +113,48 @@ export default class HaAutomationTrigger extends LitElement {
</ha-automation-trigger-row> </ha-automation-trigger-row>
` `
)} )}
<ha-button-menu
@action=${this._addTrigger}
.disabled=${this.disabled}
fixed
>
<ha-button <ha-button
slot="trigger"
outlined outlined
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.add" "ui.panel.config.automation.editor.triggers.add"
)} )}
.disabled=${this.disabled} .disabled=${this.disabled}
@click=${this._addTriggerDialog}
> >
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon> <ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button> </ha-button>
${this._clipboard?.trigger
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon">
${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`
)})
<ha-svg-icon
slot="graphic"
.path=${mdiContentPaste}
></ha-svg-icon
></mwc-list-item>`
: nothing}
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`
)}
</ha-button-menu>
</div> </div>
`; `;
} }
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<Trigger, "platform">;
};
triggers = this.triggers.concat({
platform: platform as any,
...elClass.defaultConfig,
});
}
this._focusLastTriggerOnChange = true;
fireEvent(this, "value-changed", { value: triggers });
};
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.updated(changedProps);
@ -261,30 +228,6 @@ export default class HaAutomationTrigger extends LitElement {
return this._triggerKeys.get(action)!; return this._triggerKeys.get(action)!;
} }
private _addTrigger(ev: CustomEvent<ActionDetail>) {
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<Trigger, "platform">;
};
triggers = this.triggers.concat({
platform: platform as any,
...elClass.defaultConfig,
});
}
this._focusLastTriggerOnChange = true;
fireEvent(this, "value-changed", { value: triggers });
}
private _moveUp(ev) { private _moveUp(ev) {
const index = (ev.target as any).index; const index = (ev.target as any).index;
const newIndex = index - 1; 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<typeof TRIGGER_TYPES>)
.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 { static get styles(): CSSResultGroup {
return [ return [
sortableStyles, sortableStyles,

View File

@ -109,9 +109,11 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
${this.narrow && entity.attributes.in_progress ${this.narrow && entity.attributes.in_progress
? html`<ha-circular-progress ? html`<ha-circular-progress
indeterminate indeterminate
size="small"
slot="graphic" slot="graphic"
class="absolute" class="absolute"
.ariaLabel=${this.hass.localize(
"ui.panel.config.updates.update_in_progress"
)}
></ha-circular-progress>` ></ha-circular-progress>`
: ""} : ""}
<span <span
@ -131,6 +133,9 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
indeterminate indeterminate
size="small" size="small"
slot="meta" slot="meta"
.ariaLabel=${this.hass.localize(
"ui.panel.config.updates.update_in_progress"
)}
></ha-circular-progress>` ></ha-circular-progress>`
: html`<ha-icon-next slot="meta"></ha-icon-next>` : html`<ha-icon-next slot="meta"></ha-icon-next>`
: ""} : ""}
@ -191,6 +196,8 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
} }
ha-circular-progress.absolute { ha-circular-progress.absolute {
position: absolute; position: absolute;
width: 40px;
height: 40px;
} }
state-badge.updating { state-badge.updating {
opacity: 0.5; opacity: 0.5;

View File

@ -363,8 +363,8 @@ export class HaConfigDeviceDashboard extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
type: "numeric", type: "numeric",
width: narrow ? "95px" : "15%", width: narrow ? "105px" : "15%",
maxWidth: "95px", maxWidth: "105px",
valueColumn: "battery_level", valueColumn: "battery_level",
template: (device) => { template: (device) => {
const batteryEntityPair = device.battery_entity; const batteryEntityPair = device.battery_entity;

View File

@ -121,7 +121,7 @@ const OVERRIDE_DEVICE_CLASSES = {
], ],
}; };
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]; const PRECISIONS = [0, 1, 2, 3, 4, 5, 6];

View File

@ -74,7 +74,7 @@ export interface EntityRow extends StateEntity {
entity?: HassEntity; entity?: HassEntity;
unavailable: boolean; unavailable: boolean;
restored: boolean; restored: boolean;
status: string; status: string | undefined;
area?: string; area?: string;
localized_platform: string; localized_platform: string;
} }
@ -429,7 +429,13 @@ export class HaConfigEntities extends LitElement {
? localize("ui.panel.config.entities.picker.status.unavailable") ? localize("ui.panel.config.entities.picker.status.unavailable")
: entry.disabled_by : entry.disabled_by
? localize("ui.panel.config.entities.picker.status.disabled") ? 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,
}); });
} }

View File

@ -3,21 +3,22 @@ import "@material/mwc-list/mwc-list";
import Fuse, { IFuseOptions } from "fuse.js"; import Fuse, { IFuseOptions } from "fuse.js";
import { HassConfig } from "home-assistant-js-websocket"; import { HassConfig } from "home-assistant-js-websocket";
import { import {
css,
html,
LitElement, LitElement,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
css,
html,
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { import {
protocolIntegrationPicked,
PROTOCOL_INTEGRATIONS, PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked"; } from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../../common/string/compare";
@ -34,10 +35,10 @@ import {
import { import {
Brand, Brand,
Brands, Brands,
findIntegration,
getIntegrationDescriptions,
Integration, Integration,
Integrations, Integrations,
findIntegration,
getIntegrationDescriptions,
} from "../../../data/integrations"; } from "../../../data/integrations";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { import {
@ -424,8 +425,7 @@ class AddIntegrationDialog extends LitElement {
private _renderAll(integrations?: IntegrationListItem[]): TemplateResult { private _renderAll(integrations?: IntegrationListItem[]): TemplateResult {
return html`<search-input return html`<search-input
.hass=${this.hass} .hass=${this.hass}
autofocus dialogInitialFocus=${ifDefined(this._narrow ? undefined : "")}
dialogInitialFocus
.filter=${this._filter} .filter=${this._filter}
@value-changed=${this._filterChanged} @value-changed=${this._filterChanged}
.label=${this.hass.localize( .label=${this.hass.localize(
@ -434,7 +434,9 @@ class AddIntegrationDialog extends LitElement {
@keypress=${this._maybeSubmit} @keypress=${this._maybeSubmit}
></search-input> ></search-input>
${integrations ${integrations
? html`<mwc-list> ? html`<mwc-list
dialogInitialFocus=${ifDefined(this._narrow ? "" : undefined)}
>
<lit-virtualizer <lit-virtualizer
scroller scroller
class="ha-scrollbar" class="ha-scrollbar"

View File

@ -487,7 +487,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
<h1 class="card-header"> <h1 class="card-header">
${this._manifest?.integration_type ${this._manifest?.integration_type
? this.hass.localize( ? 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( : this.hass.localize(
`ui.panel.config.integrations.integration_page.entries` `ui.panel.config.integrations.integration_page.entries`
@ -507,7 +507,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
<ha-button @click=${this._addIntegration}> <ha-button @click=${this._addIntegration}>
${this._manifest?.integration_type ${this._manifest?.integration_type
? this.hass.localize( ? 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( : this.hass.localize(
`ui.panel.config.integrations.integration_page.add_entry` `ui.panel.config.integrations.integration_page.add_entry`

View File

@ -1,5 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-textarea";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -20,6 +19,7 @@ import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant, Route } from "../../../../../types"; import { HomeAssistant, Route } from "../../../../../types";
import { zhaTabs } from "./zha-config-dashboard"; import { zhaTabs } from "./zha-config-dashboard";
import "./zha-device-pairing-status-card"; import "./zha-device-pairing-status-card";
import "../../../../../components/ha-textarea";
@customElement("zha-add-devices-page") @customElement("zha-add-devices-page")
class ZHAAddDevicesPage extends LitElement { class ZHAAddDevicesPage extends LitElement {
@ -146,13 +146,13 @@ class ZHAAddDevicesPage extends LitElement {
`} `}
</div> </div>
${this._showLogs ${this._showLogs
? html`<paper-textarea ? html`<ha-textarea
readonly readonly
max-rows="10"
class="log" class="log"
value=${this._formattedEvents} autogrow
.value=${this._formattedEvents}
> >
</paper-textarea>` </ha-textarea>`
: ""} : ""}
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;
@ -165,13 +165,6 @@ class ZHAAddDevicesPage extends LitElement {
private _handleMessage(message: any): void { private _handleMessage(message: any): void {
if (message.type === LOG_OUTPUT) { if (message.type === LOG_OUTPUT) {
this._formattedEvents += message.log_entry.message + "\n"; 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)) { if (message.type && DEVICE_MESSAGE_TYPES.includes(message.type)) {
this._discoveredDevices[message.device_info.ieee] = message.device_info; this._discoveredDevices[message.device_info.ieee] = message.device_info;
@ -266,6 +259,9 @@ class ZHAAddDevicesPage extends LitElement {
color: grey; color: grey;
padding-left: 16px; padding-left: 16px;
} }
ha-textarea {
width: 100%;
}
`, `,
]; ];
} }

View File

@ -1,6 +1,4 @@
import "@material/mwc-button"; 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 { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import type { HASSDomEvent } from "../../../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
@ -14,8 +12,9 @@ import {
ZHAGroup, ZHAGroup,
} from "../../../../../data/zha"; } from "../../../../../data/zha";
import "../../../../../layouts/hass-subpage"; import "../../../../../layouts/hass-subpage";
import type { ValueChangedEvent, HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section"; import "../../../ha-config-section";
import "../../../../../components/ha-textfield";
import "./zha-device-endpoint-data-table"; import "./zha-device-endpoint-data-table";
import type { ZHADeviceEndpointDataTable } from "./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 _groupName = "";
@state() private _groupId?: string;
@query("zha-device-endpoint-data-table", true) @query("zha-device-endpoint-data-table", true)
private _zhaDevicesDataTable!: ZHADeviceEndpointDataTable; private _zhaDevicesDataTable!: ZHADeviceEndpointDataTable;
@ -66,14 +67,23 @@ export class ZHAAddGroupPage extends LitElement {
"ui.panel.config.zha.groups.create_group_details" "ui.panel.config.zha.groups.create_group_details"
)} )}
</p> </p>
<paper-input <ha-textfield
type="string" type="string"
.value=${this._groupName} .value=${this._groupName}
@value-changed=${this._handleNameChange} @change=${this._handleNameChange}
placeholder=${this.hass!.localize( .placeholder=${this.hass!.localize(
"ui.panel.config.zha.groups.group_name_placeholder" "ui.panel.config.zha.groups.group_name_placeholder"
)} )}
></paper-input> ></ha-textfield>
<ha-textfield
type="number"
.value=${this._groupId}
@change=${this._handleGroupIdChange}
.placeholder=${this.hass!.localize(
"ui.panel.config.zha.groups.group_id_placeholder"
)}
></ha-textfield>
<div class="header"> <div class="header">
${this.hass.localize("ui.panel.config.zha.groups.add_members")} ${this.hass.localize("ui.panel.config.zha.groups.add_members")}
@ -131,7 +141,15 @@ export class ZHAAddGroupPage extends LitElement {
const memberParts = member.split("_"); const memberParts = member.split("_");
return { ieee: memberParts[0], endpoint_id: memberParts[1] }; return { ieee: memberParts[0], endpoint_id: memberParts[1] };
}); });
const group: ZHAGroup = await addGroup(this.hass, this._groupName, members); const groupId = this._groupId
? parseInt(this._groupId as string, 10)
: undefined;
const group: ZHAGroup = await addGroup(
this.hass,
this._groupName,
groupId,
members
);
this._selectedDevicesToAdd = []; this._selectedDevicesToAdd = [];
this._processingAdd = false; this._processingAdd = false;
this._groupName = ""; this._groupName = "";
@ -139,9 +157,12 @@ export class ZHAAddGroupPage extends LitElement {
navigate(`/config/zha/group/${group.group_id}`, { replace: true }); navigate(`/config/zha/group/${group.group_id}`, { replace: true });
} }
private _handleNameChange(ev: ValueChangedEvent<string>) { private _handleGroupIdChange(event) {
const target = ev.currentTarget as PaperInputElement; this._groupId = event.target.value;
this._groupName = target.value || ""; }
private _handleNameChange(event) {
this._groupName = event.target.value || "";
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -1,5 +1,4 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-input/paper-input";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -15,6 +14,7 @@ import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/buttons/ha-progress-button"; import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../../../components/ha-select"; import "../../../../../components/ha-select";
import "../../../../../components/ha-textfield";
import { forwardHaptic } from "../../../../../data/haptics"; import { forwardHaptic } from "../../../../../data/haptics";
import { import {
Attribute, Attribute,
@ -27,11 +27,7 @@ import {
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { formatAsPaddedHex } from "./functions"; import { formatAsPaddedHex } from "./functions";
import { import { ItemSelectedEvent, SetAttributeServiceData } from "./types";
ChangeEvent,
ItemSelectedEvent,
SetAttributeServiceData,
} from "./types";
@customElement("zha-cluster-attributes") @customElement("zha-cluster-attributes")
export class ZHAClusterAttributes extends LitElement { export class ZHAClusterAttributes extends LitElement {
@ -101,24 +97,28 @@ export class ZHAClusterAttributes extends LitElement {
private _renderAttributeInteractions(): TemplateResult { private _renderAttributeInteractions(): TemplateResult {
return html` return html`
<div class="input-text"> <div class="input-text">
<paper-input <ha-textfield
label=${this.hass!.localize("ui.panel.config.zha.common.value")} .label=${this.hass!.localize("ui.panel.config.zha.common.value")}
type="string" type="string"
.value=${this._attributeValue} .value=${this._attributeValue}
@value-changed=${this._onAttributeValueChanged} @change=${this._onAttributeValueChanged}
placeholder=${this.hass!.localize("ui.panel.config.zha.common.value")} .placeholder=${this.hass!.localize(
></paper-input> "ui.panel.config.zha.common.value"
)}
></ha-textfield>
</div> </div>
<div class="input-text"> <div class="input-text">
<paper-input <ha-textfield
label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.config.zha.common.manufacturer_code_override" "ui.panel.config.zha.common.manufacturer_code_override"
)} )}
type="number" type="number"
.value=${this._manufacturerCodeOverride} .value=${this._manufacturerCodeOverride}
@value-changed=${this._onManufacturerCodeOverrideChanged} @change=${this._onManufacturerCodeOverrideChanged}
placeholder=${this.hass!.localize("ui.panel.config.zha.common.value")} .placeholder=${this.hass!.localize(
></paper-input> "ui.panel.config.zha.common.value"
)}
></ha-textfield>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button <ha-progress-button
@ -197,13 +197,13 @@ export class ZHAClusterAttributes extends LitElement {
}; };
} }
private _onAttributeValueChanged(value: ChangeEvent): void { private _onAttributeValueChanged(event): void {
this._attributeValue = value.detail!.value; this._attributeValue = event.target!.value;
this._setAttributeServiceData = this._computeSetAttributeServiceData(); this._setAttributeServiceData = this._computeSetAttributeServiceData();
} }
private _onManufacturerCodeOverrideChanged(value: ChangeEvent): void { private _onManufacturerCodeOverrideChanged(event): void {
this._manufacturerCodeOverride = value.detail!.value; this._manufacturerCodeOverride = event.target!.value;
this._setAttributeServiceData = this._computeSetAttributeServiceData(); this._setAttributeServiceData = this._computeSetAttributeServiceData();
} }
@ -238,7 +238,8 @@ export class ZHAClusterAttributes extends LitElement {
margin-top: 16px; margin-top: 16px;
} }
.menu { .menu,
ha-textfield {
width: 100%; width: 100%;
} }

View File

@ -1,5 +1,4 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-input/paper-input";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -14,6 +13,7 @@ import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../../../components/ha-form/ha-form"; import "../../../../../components/ha-form/ha-form";
import "../../../../../components/ha-select"; import "../../../../../components/ha-select";
import "../../../../../components/ha-textfield";
import { import {
Cluster, Cluster,
Command, Command,
@ -23,7 +23,7 @@ import {
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { formatAsPaddedHex } from "./functions"; import { formatAsPaddedHex } from "./functions";
import { ChangeEvent, IssueCommandServiceData } from "./types"; import { IssueCommandServiceData } from "./types";
export class ZHAClusterCommands extends LitElement { export class ZHAClusterCommands extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@ -88,17 +88,17 @@ export class ZHAClusterCommands extends LitElement {
${this._selectedCommandId !== undefined ${this._selectedCommandId !== undefined
? html` ? html`
<div class="input-text"> <div class="input-text">
<paper-input <ha-textfield
label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.config.zha.common.manufacturer_code_override" "ui.panel.config.zha.common.manufacturer_code_override"
)} )}
type="number" type="number"
.value=${this._manufacturerCodeOverride} .value=${this._manufacturerCodeOverride}
@value-changed=${this._onManufacturerCodeOverrideChanged} @change=${this._onManufacturerCodeOverrideChanged}
placeholder=${this.hass!.localize( .placeholder=${this.hass!.localize(
"ui.panel.config.zha.common.value" "ui.panel.config.zha.common.value"
)} )}
></paper-input> ></ha-textfield>
</div> </div>
<div class="command-form"> <div class="command-form">
<ha-form <ha-form
@ -180,8 +180,8 @@ export class ZHAClusterCommands extends LitElement {
this._computeIssueClusterCommandServiceData(); this._computeIssueClusterCommandServiceData();
} }
private _onManufacturerCodeOverrideChanged(value: ChangeEvent): void { private _onManufacturerCodeOverrideChanged(event): void {
this._manufacturerCodeOverride = value.detail!.value; this._manufacturerCodeOverride = Number(event.target.value);
this._issueClusterCommandServiceData = this._issueClusterCommandServiceData =
this._computeIssueClusterCommandServiceData(); this._computeIssueClusterCommandServiceData();
} }
@ -199,7 +199,8 @@ export class ZHAClusterCommands extends LitElement {
ha-select { ha-select {
margin-top: 16px; margin-top: 16px;
} }
.menu { .menu,
ha-textfield {
width: 100%; width: 100%;
} }

View File

@ -1,4 +1,3 @@
import "@polymer/paper-input/paper-input";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -11,6 +10,7 @@ import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/entity/state-badge"; import "../../../../../components/entity/state-badge";
import "../../../../../components/ha-area-picker"; import "../../../../../components/ha-area-picker";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../../../components/ha-textfield";
import { updateDeviceRegistryEntry } from "../../../../../data/device_registry"; import { updateDeviceRegistryEntry } from "../../../../../data/device_registry";
import { import {
EntityRegistryEntry, EntityRegistryEntry,
@ -98,14 +98,14 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
: "" : ""
)} )}
</div> </div>
<paper-input <ha-textfield
type="string" type="string"
@change=${this._rename} @change=${this._rename}
.value=${this.device.user_given_name || this.device.name} .value=${this.device.user_given_name || this.device.name}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.zha_device_info.zha_device_card.device_name_placeholder" "ui.dialogs.zha_device_info.zha_device_card.device_name_placeholder"
)} )}
></paper-input> ></ha-textfield>
<ha-area-picker <ha-area-picker
.hass=${this.hass} .hass=${this.hass}
.device=${this.device.device_reg_id} .device=${this.device.device_reg_id}
@ -229,6 +229,9 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
ha-card { ha-card {
border: none; border: none;
} }
ha-textfield {
width: 100%;
}
`, `,
]; ];
} }

View File

@ -50,7 +50,6 @@ import "../../../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types"; import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node"; import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node";
import { showZWaveJSRebuildNetworkRoutesDialog } from "./show-dialog-zwave_js-rebuild-network-routes"; import { showZWaveJSRebuildNetworkRoutesDialog } from "./show-dialog-zwave_js-rebuild-network-routes";
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node"; import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
@ -128,16 +127,6 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
.path=${mdiRefresh} .path=${mdiRefresh}
.label=${this.hass!.localize("ui.common.refresh")} .label=${this.hass!.localize("ui.common.refresh")}
></ha-icon-button> ></ha-icon-button>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.zwave_js.dashboard.header")}
</div>
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.introduction"
)}
</div>
${this._network && ${this._network &&
this._status === "connected" && this._status === "connected" &&
(this._network?.controller.inclusion_state === (this._network?.controller.inclusion_state ===
@ -186,6 +175,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
${this._status !== "disconnected" ${this._status !== "disconnected"
? html` ? html`
<div class="details"> <div class="details">
Z-Wave
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.zwave_js.common.network" "ui.panel.config.zwave_js.common.network"
)} )}
@ -196,8 +186,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
${this.hass.localize( ${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.devices`, `ui.panel.config.zwave_js.dashboard.devices`,
{ {
count: count: this._network.controller.nodes.length,
this._network.controller.nodes.length,
} }
)} )}
${notReadyDevices > 0 ${notReadyDevices > 0
@ -224,9 +213,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`} href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
> >
<mwc-button> <mwc-button>
${this.hass.localize( ${this.hass.localize("ui.panel.config.entities.caption")}
"ui.panel.config.entities.caption"
)}
</mwc-button> </mwc-button>
</a> </a>
${this._provisioningEntries?.length ${this._provisioningEntries?.length
@ -463,12 +450,12 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
</div> </div>
<div class="card-content"> <div class="card-content">
<p> <p>
Enable the reporting of anonymized telemetry and Enable the reporting of anonymized telemetry and statistics
statistics to the <em>Z-Wave JS organization</em>. This to the <em>Z-Wave JS organization</em>. This data will be
data will be used to focus development efforts and improve used to focus development efforts and improve the user
the user experience. Information about the data that is experience. Information about the data that is collected and
collected and how it is used, including an example of the how it is used, including an example of the data collected,
data collected, can be found in the can be found in the
<a <a
target="_blank" target="_blank"
href="https://zwave-js.github.io/node-zwave-js/#/data-collection/data-collection" href="https://zwave-js.github.io/node-zwave-js/#/data-collection/data-collection"
@ -479,7 +466,6 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
</ha-card> </ha-card>
` `
: ``} : ``}
</ha-config-section>
<ha-fab <ha-fab
slot="fab" slot="fab"
.label=${this.hass.localize( .label=${this.hass.localize(

View File

@ -3,12 +3,20 @@ import {
mdiDelete, mdiDelete,
mdiHelpCircle, mdiHelpCircle,
mdiInformationOutline, mdiInformationOutline,
mdiPalette,
mdiPencilOff, mdiPencilOff,
mdiPlay, mdiPlay,
mdiPlus, mdiPlus,
} from "@mdi/js"; } from "@mdi/js";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { differenceInDays } from "date-fns/esm"; import { differenceInDays } from "date-fns/esm";
@ -21,6 +29,7 @@ import {
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button-related-filter-menu"; import "../../../components/ha-button-related-filter-menu";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-button";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
@ -214,6 +223,7 @@ class HaSceneDashboard extends LitElement {
.columns=${this._columns(this.hass.locale, this.narrow)} .columns=${this._columns(this.hass.locale, this.narrow)}
id="entity_id" id="entity_id"
.data=${this._scenes(this.scenes, this._filteredScenes)} .data=${this._scenes(this.scenes, this._filteredScenes)}
.empty=${!this.scenes.length}
.activeFilters=${this._activeFilters} .activeFilters=${this._activeFilters}
.noDataText=${this.hass.localize( .noDataText=${this.hass.localize(
"ui.panel.config.scene.picker.no_scenes" "ui.panel.config.scene.picker.no_scenes"
@ -238,6 +248,30 @@ class HaSceneDashboard extends LitElement {
@related-changed=${this._relatedFilterChanged} @related-changed=${this._relatedFilterChanged}
> >
</ha-button-related-filter-menu> </ha-button-related-filter-menu>
${!this.scenes.length
? html`<div class="empty" slot="empty">
<ha-svg-icon .path=${mdiPalette}></ha-svg-icon>
<h1>
${this.hass.localize(
"ui.panel.config.scene.picker.empty_header"
)}
</h1>
<p>
${this.hass.localize("ui.panel.config.scene.picker.empty_text")}
</p>
<a
href=${documentationUrl(this.hass, "/docs/scene/editor/")}
target="_blank"
rel="noreferrer"
>
<ha-button>
${this.hass.localize(
"ui.panel.config.scene.picker.learn_more"
)}
</ha-button>
</a>
</div>`
: nothing}
<a href="/config/scene/edit/new" slot="fab"> <a href="/config/scene/edit/new" slot="fab">
<ha-fab <ha-fab
.label=${this.hass.localize( .label=${this.hass.localize(
@ -336,7 +370,7 @@ class HaSceneDashboard extends LitElement {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
${this.hass.localize("ui.panel.config.scene.picker.learn_more")} ${this.hass.localize("ui.panel.config.common.learn_more")}
</a> </a>
</p> </p>
`, `,
@ -350,6 +384,11 @@ class HaSceneDashboard extends LitElement {
a { a {
text-decoration: none; text-decoration: none;
} }
.empty {
--paper-font-headline_-_font-size: 28px;
--mdc-icon-size: 80px;
max-width: 500px;
}
`, `,
]; ];
} }

View File

@ -538,7 +538,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
value.valid value.valid
? "" ? ""
: html`${this.hass.localize( : html`${this.hass.localize(
`ui.panel.config.automation.editor.${key}s.header` `ui.panel.config.automation.editor.${key}s.name`
)}: )}:
${value.error}<br />` ${value.error}<br />`
); );

View File

@ -5,10 +5,18 @@ import {
mdiInformationOutline, mdiInformationOutline,
mdiPlay, mdiPlay,
mdiPlus, mdiPlus,
mdiScriptText,
mdiTransitConnection, mdiTransitConnection,
} from "@mdi/js"; } from "@mdi/js";
import { differenceInDays } from "date-fns/esm"; import { differenceInDays } from "date-fns/esm";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; import {
CSSResultGroup,
LitElement,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -241,6 +249,7 @@ class HaScriptPicker extends LitElement {
.tabs=${configSections.automations} .tabs=${configSections.automations}
.columns=${this._columns(this.narrow, this.hass.locale)} .columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._scripts(this.scripts, this._filteredScripts)} .data=${this._scripts(this.scripts, this._filteredScripts)}
.empty=${!this.scripts.length}
.activeFilters=${this._activeFilters} .activeFilters=${this._activeFilters}
id="entity_id" id="entity_id"
.noDataText=${this.hass.localize( .noDataText=${this.hass.localize(
@ -266,6 +275,32 @@ class HaScriptPicker extends LitElement {
@related-changed=${this._relatedFilterChanged} @related-changed=${this._relatedFilterChanged}
> >
</ha-button-related-filter-menu> </ha-button-related-filter-menu>
${!this.scripts.length
? html` <div class="empty" slot="empty">
<ha-svg-icon .path=${mdiScriptText}></ha-svg-icon>
<h1>
${this.hass.localize(
"ui.panel.config.script.picker.empty_header"
)}
</h1>
<p>
${this.hass.localize(
"ui.panel.config.script.picker.empty_text"
)}
</p>
<a
href=${documentationUrl(this.hass, "/docs/script/editor/")}
target="_blank"
rel="noreferrer"
>
<ha-button>
${this.hass.localize(
"ui.panel.config.script.picker.learn_more"
)}
</ha-button>
</a>
</div>`
: nothing}
<ha-fab <ha-fab
slot="fab" slot="fab"
?is-wide=${this.isWide} ?is-wide=${this.isWide}
@ -385,7 +420,7 @@ class HaScriptPicker extends LitElement {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
${this.hass.localize("ui.panel.config.script.picker.learn_more")} ${this.hass.localize("ui.panel.config.common.learn_more")}
</a> </a>
</p> </p>
`, `,
@ -471,6 +506,11 @@ class HaScriptPicker extends LitElement {
a { a {
text-decoration: none; text-decoration: none;
} }
.empty {
--paper-font-headline_-_font-size: 28px;
--mdc-icon-size: 80px;
max-width: 500px;
}
`, `,
]; ];
} }

View File

@ -5,7 +5,6 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown";
import "../../../components/ha-select"; import "../../../components/ha-select";
import { import {
extractApiErrorMessage, extractApiErrorMessage,

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