Compare commits

...

85 Commits

Author SHA1 Message Date
Bram Kragten
9d9e789f4b 20231227.0 (#19157) 2023-12-27 17:29:11 +01:00
Bram Kragten
1a0e3890f4 Bumped version to 20231227.0 2023-12-27 17:22:34 +01:00
Bram Kragten
caece9d6ad Add QR code element (#19155)
* Add QR code element

* fixes

* move to workflow

* limit webpack imports
2023-12-27 17:22:09 +01:00
renovate[bot]
953a3793c4 Update dependency vue to v2.7.16 (#19156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 17:19:27 +01:00
Jan-Philipp Benecke
8bfae3b4cf Fix ha-combo-box helper text not displaying text (#19154)
* Fix ha-combo-box helper text

* Update src/components/ha-combo-box.ts

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-12-27 15:22:19 +00:00
Bram Kragten
6911685bd0 Only focus search when not in fullscreen dialog (#19153) 2023-12-27 12:45:49 +00:00
Bram Kragten
71025eaf4d Fix dialog headers (#19152)
* Fix dialog headers

* increase padding
2023-12-27 13:45:36 +01:00
Bram Kragten
3aa612b766 Todo list layout fixes (#19150) 2023-12-27 13:22:36 +01:00
Simon Lamon
a2ffd0ae83 Circular progress improvement in update area (#18983)
* Fixes

* Add aria label again
2023-12-27 11:07:40 +01:00
Simon Lamon
c399da586e Increase battery column width (#19143) 2023-12-27 11:00:48 +01:00
Erik Montnemery
d7826e4e6c Show +/- volume buttons for media players which support VOLUME_SET (#19111)
* Show +/- volume buttons for media players which support VOLUME_SET

* Fixup
2023-12-27 11:00:12 +01:00
Yosi Levy
f5d13c9079 Various RTL fixes (#19134)
* Various RTL fixes

* Various RTL fixes
2023-12-27 10:59:28 +01:00
Paul Bottein
01a142790f Add update actions card feature (#19110)
* Add update tile feature

* Fix translations

* Add confirmation dialog

* Remove unused styles

* Fix gallery

* Update wording

* Update src/translations/en.json
2023-12-27 10:55:14 +01:00
karwosts
df54687de1 Fix trailing energy gaps, refactor chart options (#19117)
* Fix trailing gap on energy graph cards

* Use date methods instead of unix time calculation

* Fix trailing energy gaps, refactor chart options

---------

Co-authored-by: Till Fleisch <till@fleisch.dev>
2023-12-27 10:53:16 +01:00
karwosts
bded31b311 Fix entities sort for hidden/readonly (#19124) 2023-12-27 10:51:36 +01:00
c0ffeeca7
67e573aff7 HVAC in demo - fix typo (#19132) 2023-12-27 10:25:30 +01:00
c0ffeeca7
6295c4ac76 Add Oxford comma (#19133) 2023-12-27 10:24:57 +01:00
renovate[bot]
469811847f Lock file maintenance (#19084)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-26 11:53:07 -05:00
renovate[bot]
8a1aefefca Update vaadinWebComponents monorepo to v24.3.2 (#19141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-25 15:39:49 -05:00
renovate[bot]
bea16028a1 Update dependency open to v10.0.1 (#19145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-25 15:38:06 -05:00
dependabot[bot]
bedb7d1d9e Bump actions/setup-node from 4.0.0 to 4.0.1 (#19140)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-25 15:37:25 -05:00
renovate[bot]
1a3a20f478 Update dependency element-internals-polyfill to v1.3.10 (#19136) 2023-12-24 23:25:00 -05:00
Bram Kragten
68ecb7c219 Store height and width of dialog, autofocus searchbar (#19122) 2023-12-23 00:03:52 +01:00
renovate[bot]
61b04a882b Update dependency core-js to v3.34.0 (#18974)
* Update dependency core-js to v3.34.0

* Update Babel setting with package.json version

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-12-22 15:41:33 -05:00
renovate[bot]
f8e621c5b9 Update dependency open to v10 (#19116)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-22 14:16:41 -05:00
Bram Kragten
2f2209682e Update learn more URLs to just "Learn more" 2023-12-22 16:40:20 +01:00
Bram Kragten
f4f361b51a Add automation picker descriptions (#19121) 2023-12-22 16:35:41 +01:00
Erik Montnemery
55e59f8cb0 Add valve to switch_as_x domains (#19118) 2023-12-22 11:34:09 +01:00
renovate[bot]
6d20ed0a22 Update dependency hls.js to v1.4.14 (#19112) 2023-12-21 20:22:36 -05:00
renovate[bot]
874f604295 Update typescript-eslint monorepo to v6.15.0 (#19113) 2023-12-21 20:21:23 -05:00
renovate[bot]
30d36a11c1 Update dependency eslint-plugin-lit to v1.11.0 (#19103) 2023-12-21 20:19:36 -05:00
Bram Kragten
09dcc29175 Add description and due support to todo lists (#19107) 2023-12-21 21:30:24 +01:00
Bram Kragten
8f07e6f141 Group add automation elements in dialog (#19086)
* Group add automation elements in dialog

* Add search

* clear filter on close

* Split out services

* group services by integration type

* Update add-automation-element-dialog.ts

* fix typing

* clear filter on back

* Update add-automation-element-dialog.ts

* Fix search

* scroll to top

* Add service descriptions

* fix clipboard

* Move play media, sort services

* use helpers

* move to data

* Move building blocks to a group

* fix search

* Update add-automation-element-dialog.ts

* Update en.json

* fix alignment of single line and multi line items

* use repeat instead of map
2023-12-21 21:01:27 +01:00
Bram Kragten
7b6b5724e1 Add descriptions to automation page sections (#19081)
* Add descriptions to automation page sections

* add name key

* Update manual-automation-editor.ts

* hide explanation if there is content

* Update src/translations/en.json

* hide when advanced
2023-12-21 21:01:11 +01:00
Quentame
521c0b58c8 Add climate fan mode feature to thermostat & tile cards (#19094)
* climate: Add fan_only exemple

* climate: add fan_mode feature to thermostat & tile cards

* review: update dropdown icon
2023-12-21 15:49:08 +01:00
Bram Kragten
53839ab7b1 Update text on empty automation/script/scene pages (#19095) 2023-12-21 15:36:56 +01:00
Chris Roberts
dcfe9617b3 Make disabled sliders properly visible (#19102)
Followed similar variable usage by ha-*-chip components.

Fixes #19101
2023-12-20 21:26:48 +00:00
Matthias Alphart
58eebf2dbd Clear Area picker when "Add new area" was canceled (#19088) 2023-12-20 22:17:31 +01:00
Paul Bottein
af9b64c6f0 Add option to show current temperature on thermostat card (#19049)
* Fix unit position when no decimal

* Add option to switch between current and target for thermostat card

* Refactor code

* Clean label code

* Rename config name
2023-12-20 14:41:22 +01:00
Paul Bottein
2b18ac8d4e Add option to show current humidity on humidifier card (#19079) 2023-12-20 14:12:59 +01:00
karwosts
2306234063 Localize date-range-picker (#18945) 2023-12-19 09:25:56 +01:00
renovate[bot]
883a9e422e Update dependency eslint to v8.56.0 (#19083) 2023-12-18 21:24:47 -05:00
Dmitry Tsydzik
d77ce721e3 Add possibility to define group id manually for ZHA (#18932)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-12-18 15:37:57 +01:00
Bram Kragten
325ad6f721 Fix step any in number selector (#19077) 2023-12-18 15:24:46 +01:00
Simon Lamon
d762a9365f Automation traces localization (#18862) 2023-12-18 15:06:41 +01:00
Paul Bottein
ce983f043e Fix empty classmap for thermostat card (#19078) 2023-12-18 14:00:37 +01:00
Bram Kragten
45b7ebbe46 Add empty page to automation/script/scene config (#19075) 2023-12-18 13:59:36 +01:00
Paul Bottein
3e7fa66790 Add valve entity (#19024)
* Add valve entity

* Update icon based on device class

* Check assumed state first

* Reset mode if entity id changes
2023-12-18 13:53:52 +01:00
renovate[bot]
ad543dbffb Update dependency webpackbar to v6 (#19076)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-18 12:31:13 +00:00
karwosts
cdd2c7be9a Fix date-range-picker overflow on energy dashboard small screen (#19026) 2023-12-18 13:20:21 +01:00
Simon Lamon
ddf6945190 Localize state condition in automation editor (#18864)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-12-18 13:19:19 +01:00
Simon Lamon
f2745747ba Replace more polymer paper inputs and remove paper-input style attributes (#18929) 2023-12-18 13:05:56 +01:00
Charles Garwood
ff9d179c13 Minor tweak to Z-Wave JS Config Dashboard layout (#19032) 2023-12-18 12:56:30 +01:00
karwosts
e813108c66 Optional boolean service field defaults to false (#19043) 2023-12-18 12:56:01 +01:00
karwosts
62d8cdfcf9 Fix missing tooltips in energy-usage-graph (#19031) 2023-12-18 12:52:52 +01:00
karwosts
5b9d46e350 Add a safe mode item to the github issue template checkist (#19022) 2023-12-18 12:49:03 +01:00
renovate[bot]
e5db95b2d2 Update vaadinWebComponents monorepo to v24.3.1 (#19052)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-18 12:08:21 +01:00
karwosts
a8f7c7c999 Fix hide legend toggle in stats graph editor (#19021) 2023-12-18 11:55:50 +01:00
c0ffeeca7
8a423fb775 Assist, no wake word: point docs link to wake word installation proce… (#19040)
Assist, no wake word: point docs link to wake word installation procedure
2023-12-18 11:47:32 +01:00
c0ffeeca7
90c3d69af6 No supervisor message: fix typo (#19041) 2023-12-18 11:46:44 +01:00
renovate[bot]
bcb2d73c5c Update dependency @material/web to v1.1.1 (#19064)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-18 11:44:20 +01:00
dependabot[bot]
8a6cea12e1 Bump github/codeql-action from 2 to 3 (#19072)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 09:23:52 +01:00
renovate[bot]
0056de146a Update dependency @codemirror/view to v6.22.3 (#19063)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-17 19:04:01 -05:00
renovate[bot]
0bcdbef11e Update dependency eslint-plugin-import to v2.29.1 (#19070)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-17 19:02:58 -05:00
renovate[bot]
1b208cb531 Update dependency marked to v11.1.0 (#19054) 2023-12-15 21:12:42 -05:00
renovate[bot]
6a975e260c Update dependency @rollup/plugin-json to v6.1.0 (#19057) 2023-12-15 21:10:09 -05:00
renovate[bot]
02d2bde269 Update dependency @babel/helper-define-polyfill-provider to v0.4.4 (#19047) 2023-12-15 01:52:14 +00:00
renovate[bot]
70cfa7f48a Update babel monorepo to v7.23.6 (#19046) 2023-12-14 20:41:13 -05:00
renovate[bot]
b7d4b9c21b Update typescript-eslint monorepo to v6.14.0 (#19051) 2023-12-14 20:35:42 -05:00
renovate[bot]
673c947c11 Update dependency lit-analyzer to v2.0.2 (#19053) 2023-12-14 20:34:40 -05:00
Arno
2682a55148 Fixing issue #10581 Automation Debug Trace Graph not displaying right in Safari (#19036) 2023-12-14 11:53:53 +01:00
renovate[bot]
616f6ddf5f Update dependency @koa/cors to v5 [SECURITY] (#19018) 2023-12-14 02:38:00 +00:00
renovate[bot]
29769813bc Update dependency @codemirror/view to v6.22.2 (#19016) 2023-12-14 01:58:39 +00:00
renovate[bot]
221c46344a Update dependency rollup-plugin-visualizer to v5.11.0 (#19025) 2023-12-13 20:46:12 -05:00
renovate[bot]
a33eb25c92 Update dependency prettier to v3.1.1 (#19027) 2023-12-13 20:45:00 -05:00
Simon Lamon
1dc61320a6 Fix Github Action labeler configuration (#19023) 2023-12-12 20:35:21 +01:00
karwosts
ad556a43f9 Sort default dashboard area cards by alphabetical order if no order specified (#18989) 2023-12-12 14:25:12 +01:00
Paul Bottein
6ce613acd2 20231208.2 (#18971) 2023-12-08 14:50:28 +01:00
Paul Bottein
aa38e2d409 20231208.1 (#18962) 2023-12-08 10:36:45 +01:00
Paul Bottein
fce4e5e382 20231206.0 (#18925) 2023-12-06 14:24:48 +01:00
Bram Kragten
eb5e7ba3f3 20231205.0 (#18916) 2023-12-05 18:10:37 +01:00
Bram Kragten
ae2e8e7402 20231204.0 (#18882) 2023-12-04 12:10:33 +01:00
Bram Kragten
b854d23431 20231130.0 (#18843) 2023-11-30 17:19:47 +01:00
Bram Kragten
ef735d65cf 20231129.1 (#18811) 2023-11-29 15:31:24 +01:00
Bram Kragten
2803e6aa95 20231129.0 (#18809) 2023-11-29 12:53:12 +01:00
140 changed files with 6397 additions and 3055 deletions

View File

@@ -24,6 +24,7 @@ body:
required: true
- label: I have tried a different browser to see if it is related to my browser.
required: true
- label: I have tried reproducing the issue in [safe mode](https://www.home-assistant.io/blog/2023/11/01/release-202311/#restarting-into-safe-mode) to rule out problems with unsupported custom resources.
- type: markdown
attributes:
value: |

50
.github/labeler.yml vendored
View File

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

View File

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

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,14 +57,14 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Build resources
run: ./node_modules/.bin/gulp build-translations build-locale-data
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data
- name: Run Tests
run: yarn run test
build:
@@ -75,7 +75,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -99,7 +99,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.0
uses: actions/setup-node@v4.0.1
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -1,6 +1,7 @@
const path = require("path");
const env = require("./env.cjs");
const paths = require("./paths.cjs");
const { dependencies } = require("../package.json");
// GitHub base URL to use for production source maps
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
@@ -90,7 +91,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
"@babel/preset-env",
{
useBuiltIns: latestBuild ? false : "usage",
corejs: latestBuild ? false : "3.33",
corejs: latestBuild ? false : dependencies["core-js"],
bugfixes: true,
shippedProposals: true,
},
@@ -140,7 +141,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
// Import helpers and regenerator from runtime package
[
"@babel/plugin-transform-runtime",
{ version: require("../package.json").dependencies["@babel/runtime"] },
{ version: dependencies["@babel/runtime"] },
],
// Support some proposals still in TC39 process
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],

View File

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

View File

@@ -35,6 +35,18 @@ const ENTITIES = [
friendly_name: "Nest",
supported_features: 43,
}),
getEntity("climate", "sensibo", "fan_only", {
current_temperature: null,
temperature: null,
min_temp: 0,
max_temp: 1,
target_temp_step: 1,
hvac_modes: ["fan_only", "off"],
friendly_name: "Sensibo purifier",
fan_modes: ["low", "high"],
fan_mode: "low",
supported_features: 9,
}),
getEntity("climate", "unavailable", "unavailable", {
supported_features: 43,
}),
@@ -57,6 +69,23 @@ const CONFIGS = [
entity: climate.nest
`,
},
{
heading: "Fan only example",
config: `
- type: thermostat
entity: climate.sensibo
features:
- type: climate-hvac-modes
hvac_modes:
- fan_only
- 'off'
- type: climate-fan-modes
style: icons
fan_modes:
- low
- high
`,
},
{
heading: "Unavailable",
config: `

View File

@@ -31,6 +31,21 @@ const ENTITIES = [
max_temp: 30,
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
}),
getEntity("climate", "fan", "fan_only", {
friendly_name: "Basic fan",
hvac_modes: ["fan_only", "off"],
hvac_mode: "fan_only",
fan_modes: ["low", "high"],
fan_mode: "low",
current_temperature: null,
temperature: null,
min_temp: 0,
max_temp: 1,
target_temp_step: 1,
supported_features:
// eslint-disable-next-line no-bitwise
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE,
}),
getEntity("climate", "hvac", "auto", {
friendly_name: "Basic hvac",
hvac_modes: ["auto", "off"],

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20231208.2"
version = "20231227.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
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,
mdiLockClock,
mdiLockOpen,
mdiMeterGas,
mdiMotionSensor,
mdiPackage,
mdiPackageDown,
mdiPackageUp,
mdiPipeValve,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
@@ -274,6 +276,16 @@ export const domainIconWithoutDefault = (
: mdiPackageUp
: mdiPackage;
case "valve":
switch (stateObj?.attributes.device_class) {
case "water":
return mdiPipeValve;
case "gas":
return mdiMeterGas;
default:
return mdiPipeValve;
}
case "water_heater":
return compareState === "off" ? mdiWaterBoilerOff : mdiWaterBoiler;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -36,17 +36,24 @@ export class HaListItem extends ListItemBase {
--mdc-list-item-graphic-margin,
16px
) !important;
direction: var(--direction);
direction: var(--direction) !important;
}
span.material-icons:last-of-type {
margin-inline-start: auto !important;
margin-inline-end: 0px !important;
direction: var(--direction);
direction: var(--direction) !important;
}
.mdc-deprecated-list-item__meta {
display: var(--mdc-list-item-meta-display);
align-items: center;
}
:host([graphic="icon"]:not([twoline]))
.mdc-deprecated-list-item__graphic {
margin-inline-end: var(
--mdc-list-item-graphic-margin,
20px
) !important;
}
:host([multiline-secondary]) {
height: auto;
}
@@ -78,6 +85,15 @@ export class HaListItem extends ListItemBase {
pointer-events: unset;
}
`,
// safari workaround - must be explicit
document.dir === "rtl"
? css`
span.material-icons:first-of-type,
span.material-icons:last-of-type {
direction: rtl !important;
}
`
: css``,
];
}
}

View File

@@ -95,6 +95,15 @@ class HaMarkdownElement extends ReactiveElement {
}
node.firstElementChild!.replaceWith(alertNote);
}
} else if (
node instanceof HTMLElement &&
["ha-alert", "ha-qr-code", "ha-icon", "ha-svg-icon"].includes(
node.localName
)
) {
import(
/* webpackInclude: /(ha-alert)|(ha-qr-code)|(ha-icon)|(ha-svg-icon)/ */ `./${node.localName}`
);
}
}
}

View File

@@ -2,11 +2,6 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-markdown-element";
// Import components that are allwoed to be defined.
import "./ha-alert";
import "./ha-icon";
import "./ha-svg-icon";
@customElement("ha-markdown")
export class HaMarkdown extends LitElement {
@property() public content?;

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

View File

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

View File

@@ -522,6 +522,14 @@ export class HaServiceControl extends LitElement {
defaultValue = field.selector.constant?.value;
}
if (
defaultValue == null &&
field?.selector &&
"boolean" in field.selector
) {
defaultValue = false;
}
if (defaultValue != null) {
data = {
...this._value?.data,

View File

@@ -35,7 +35,12 @@ export class HaSettingsRow extends LitElement {
align-items: center;
}
.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;
display: var(--layout-vertical_-_display);
flex-direction: var(--layout-vertical_-_flex-direction);

View File

@@ -11,6 +11,7 @@ export class HaSlider extends MdSlider {
:host {
--md-sys-color-primary: var(--primary-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-height: 14px;
min-width: 100px;

View File

@@ -280,6 +280,8 @@ export abstract class TopAppBarBaseBase extends BaseElement {
}
#title {
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;
flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px);
@@ -290,6 +292,8 @@ export abstract class TopAppBarBaseBase extends BaseElement {
}
.pane {
border-right: 1px solid var(--divider-color);
border-inline-end: 1px solid var(--divider-color);
border-inline-start: initial;
box-sizing: border-box;
display: flex;
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 { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
@@ -18,6 +25,12 @@ import { traceTabStyles } from "./trace-tab-styles";
import { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph";
const TRACE_PATH_TABS = [
"step_config",
"changed_variables",
"logbook",
] as const;
@customElement("ha-trace-path-details")
export class HaTracePathDetails extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -34,7 +47,7 @@ export class HaTracePathDetails extends LitElement {
@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 {
return html`
@@ -43,23 +56,21 @@ export class HaTracePathDetails extends LitElement {
</div>
<div class="tabs top">
${[
["config", "Step Config"],
["changed_variables", "Changed Variables"],
["logbook", "Related logbook entries"],
].map(
([view, label]) => html`
${TRACE_PATH_TABS.map(
(view) => html`
<button
.view=${view}
class=${classMap({ active: this._view === view })}
@click=${this._showTab}
>
${label}
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.${view}`
)}
</button>
`
)}
</div>
${this._view === "config"
${this._view === "step_config"
? this._renderSelectedConfig()
: this._view === "changed_variables"
? this._renderChangedVars()
@@ -71,7 +82,9 @@ export class HaTracePathDetails extends LitElement {
const paths = this.trace.trace;
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.
@@ -82,12 +95,16 @@ export class HaTracePathDetails extends LitElement {
] as ChooseActionTraceStep[];
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)) {
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[][] = [];
@@ -115,29 +132,53 @@ export class HaTracePathDetails extends LitElement {
trace as any;
if (result?.enabled === false) {
return html`This node was disabled and skipped during execution so
no further trace information is available.`;
return html`${this.hass!.localize(
"ui.panel.config.automation.trace.path.disabled_node"
)}`;
}
return html`
${curPath === this.selected.path
? ""
: html`<h2>${curPath.substr(this.selected.path.length + 1)}</h2>`}
${data.length === 1 ? "" : html`<h3>Iteration ${idx + 1}</h3>`}
Executed:
${formatDateTimeWithSeconds(
new Date(timestamp),
this.hass.locale,
this.hass.config
)}<br />
: html`<h2>
${curPath.substring(this.selected.path.length + 1)}
</h2>`}
${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),
this.hass.locale,
this.hass.config
),
}
)}
<br />
${result
? html`Result:
? html`${this.hass!.localize(
"ui.panel.config.automation.trace.path.result"
)}
<pre>${dump(result)}</pre>`
: 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
? ""
? nothing
: html`<pre>${dump(rest)}</pre>`}
`;
})
@@ -149,16 +190,18 @@ export class HaTracePathDetails extends LitElement {
private _renderSelectedConfig() {
if (!this.selected?.path) {
return "";
return nothing;
}
const config = getDataFromPath(this.trace!.config, this.selected.path);
return config
? html`<ha-code-editor
.value=${dump(config).trimRight()}
.value=${dump(config).trimEnd()}
readOnly
dir="ltr"
></ha-code-editor>`
: "Unable to find config";
: this.hass!.localize(
"ui.panel.config.automation.trace.path.unable_to_find_config"
);
}
private _renderChangedVars() {
@@ -169,10 +212,19 @@ export class HaTracePathDetails extends LitElement {
<div class="padded-box">
${data.map(
(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
? "No variables changed"
: html`<pre>${dump(trace.changed_variables).trimRight()}</pre>`}
? this.hass!.localize(
"ui.panel.config.automation.trace.path.no_variables_changed"
)
: html`<pre>${dump(trace.changed_variables).trimEnd()}</pre>`}
`
)}
</div>
@@ -186,7 +238,11 @@ export class HaTracePathDetails extends LitElement {
const index = trackedPaths.indexOf(this.selected.path);
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[];
@@ -234,7 +290,9 @@ export class HaTracePathDetails extends LitElement {
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
`
: 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>`;
}

View File

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

View File

@@ -5,6 +5,8 @@ import {
mdiCallSplit,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiGestureDoubleTap,
mdiHandBackRight,
mdiPalette,
@@ -13,10 +15,12 @@ import {
mdiRoomService,
mdiShuffleDisabled,
mdiTimerOutline,
mdiTools,
mdiTrafficLight,
} from "@mdi/js";
import { AutomationElementGroup } from "./automation";
export const ACTION_TYPES = {
export const ACTION_ICONS = {
condition: mdiAbTesting,
delay: mdiTimerOutline,
event: mdiGestureDoubleTap,
@@ -34,6 +38,43 @@ export const ACTION_TYPES = {
variables: mdiApplicationVariableOutline,
} 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",
]);
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[]) =>
(a: string, b: string) => {
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) {
const nameA = entries?.[a]?.name ?? a;
const nameB = entries?.[b]?.name ?? b;

View File

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

View File

@@ -766,48 +766,38 @@ const tryDescribeCondition = (
// State Condition
if (condition.condition === "state") {
let base = "Confirm";
if (!condition.entity_id) {
return `${base} state`;
return hass.localize(
`${conditionsTranslationBaseKey}.state.description.no_entity`
);
}
let attribute = "";
if (condition.attribute) {
const stateObj = Array.isArray(condition.entity_id)
? hass.states[condition.entity_id[0]]
: hass.states[condition.entity_id];
base += ` ${computeAttributeNameDisplay(
attribute = computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
condition.attribute
)} of`;
);
}
const entities: string[] = [];
if (Array.isArray(condition.entity_id)) {
const entities: string[] = [];
for (const entity of condition.entity_id.values()) {
if (hass.states[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) {
base += ` ${
entities.push(
hass.states[condition.entity_id]
? computeStateName(hass.states[condition.entity_id])
: condition.entity_id
} is`;
);
}
const states: string[] = [];
@@ -845,21 +835,27 @@ const tryDescribeCondition = (
);
}
if (states.length === 0) {
states.push("a state");
}
const statesString = formatListWithOrs(hass.locale, states);
base += ` ${statesString}`;
let duration = "";
if (condition.for) {
const duration = describeDuration(hass.locale, condition.for);
if (duration) {
base += ` for ${duration}`;
}
duration = describeDuration(hass.locale, condition.for) || "";
}
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

View File

@@ -3,16 +3,21 @@ import {
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiGateOr,
mdiIdentifier,
mdiMapClock,
mdiMapMarkerRadius,
mdiNotEqualVariant,
mdiNumeric,
mdiShape,
mdiStateMachine,
mdiWeatherSunny,
} from "@mdi/js";
import { AutomationElementGroup } from "./automation";
export const CONDITION_TYPES = {
export const CONDITION_ICONS = {
device: mdiDevices,
and: mdiAmpersand,
or: mdiGateOr,
@@ -25,3 +30,23 @@ export const CONDITION_TYPES = {
trigger: mdiIdentifier,
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;
}
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 {
@@ -73,7 +73,7 @@ export function canClose(stateObj: CoverEntity): boolean {
return false;
}
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 {
@@ -85,7 +85,7 @@ export function canOpenTilt(stateObj: CoverEntity): boolean {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return !isFullyOpenTilt(stateObj) || assumedState;
return assumedState || !isFullyOpenTilt(stateObj);
}
export function canCloseTilt(stateObj: CoverEntity): boolean {
@@ -93,7 +93,7 @@ export function canCloseTilt(stateObj: CoverEntity): boolean {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return !isFullyClosedTilt(stateObj) || assumedState;
return assumedState || !isFullyClosedTilt(stateObj);
}
export function canStopTilt(stateObj: CoverEntity): boolean {

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,8 @@ export interface TodoItem {
uid: string;
summary: string;
status: TodoItemStatus;
description?: string;
due?: string;
}
export const enum TodoListEntityFeature {
@@ -25,6 +27,9 @@ export const enum TodoListEntityFeature {
DELETE_TODO_ITEM = 2,
UPDATE_TODO_ITEM = 4,
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[] =>
@@ -74,20 +79,30 @@ export const updateItem = (
hass.callService(
"todo",
"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 }
);
export const createItem = (
hass: HomeAssistant,
entity_id: string,
summary: string
item: Omit<TodoItem, "uid" | "status">
): Promise<ServiceCallResponse> =>
hass.callService(
"todo",
"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 }
);

View File

@@ -4,13 +4,16 @@ import {
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiGestureDoubleTap,
mdiMapClock,
mdiMapMarker,
mdiMapMarkerRadius,
mdiMessageAlert,
mdiMicrophoneMessage,
mdiNfcVariant,
mdiNumeric,
mdiShape,
mdiStateMachine,
mdiSwapHorizontal,
mdiWeatherSunny,
@@ -18,8 +21,9 @@ import {
} from "@mdi/js";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import { AutomationElementGroup } from "./automation";
export const TRIGGER_TYPES = {
export const TRIGGER_ICONS = {
calendar: mdiCalendar,
device: mdiDevices,
event: mdiGestureDoubleTap,
@@ -38,3 +42,26 @@ export const TRIGGER_TYPES = {
persistent_notification: mdiMessageAlert,
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 { showToast } from "../util/toast";
export const UPDATE_SUPPORT_INSTALL = 1;
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
export const UPDATE_SUPPORT_PROGRESS = 4;
export const UPDATE_SUPPORT_BACKUP = 8;
export const UPDATE_SUPPORT_RELEASE_NOTES = 16;
export enum UpdateEntityFeature {
INSTALL = 1,
SPECIFIC_VERSION = 2,
PROGRESS = 4,
BACKUP = 8,
RELEASE_NOTES = 16,
}
interface UpdateEntityAttributes extends HassEntityAttributeBase {
auto_update: boolean | null;
@@ -35,7 +37,7 @@ export interface UpdateEntity extends HassEntityBase {
}
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
typeof entity.attributes.in_progress === "number";
export const updateCanInstall = (
@@ -44,7 +46,7 @@ export const updateCanInstall = (
): boolean =>
(entity.state === BINARY_STATE_ON ||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
supportsFeature(entity, UPDATE_SUPPORT_INSTALL);
supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
updateUsesProgress(entity) || !!entity.attributes.in_progress;
@@ -176,7 +178,7 @@ export const computeUpdateStateDisplay = (
if (state === "on") {
if (updateIsInstalling(stateObj)) {
const supportsProgress =
supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS) &&
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
typeof attributes.in_progress === "number";
if (supportsProgress) {
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 = (
hass: HomeAssistant,
groupName: string,
groupId?: number,
membersToAdd?: ZHAGroupMember[]
): Promise<ZHAGroup> =>
hass.callWS({
type: "zha/group/add",
group_name: groupName,
group_id: groupId,
members: membersToAdd,
});

View File

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

View File

@@ -42,7 +42,9 @@ class MoreInfoCover extends LitElement {
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
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 =
supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION) ||
supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION)

View File

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

View File

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

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"),
update: () => import("./controls/more-info-update"),
vacuum: () => import("./controls/more-info-vacuum"),
valve: () => import("./controls/more-info-valve"),
water_heater: () => import("./controls/more-info-water_heater"),
weather: () => import("./controls/more-info-weather"),
};

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

View File

@@ -124,6 +124,12 @@ export class HaTabsSubpageDataTable extends LitElement {
*/
@property({ type: String }) public noDataText?: string;
/**
* Hides the data table and show an empty message.
* @type {Boolean}
*/
@property({ type: Boolean }) public empty = false;
@property() public route!: Route;
/**
@@ -198,56 +204,61 @@ export class HaTabsSubpageDataTable extends LitElement {
.mainPage=${this.mainPage}
.supervisor=${this.supervisor}
>
${!this.hideFilterMenu
? html`
<div slot="toolbar-icon">
${this.narrow
${this.empty
? html`<div class="center">
<slot name="empty">${this.noDataText}</slot>
</div>`
: html`${!this.hideFilterMenu
? html`
<div slot="toolbar-icon">
${this.narrow
? html`
<div class="filter-menu">
${this.numHidden || this.activeFilters
? html`<span class="badge"
>${this.numHidden || "!"}</span
>`
: ""}
<slot name="filter-menu"></slot>
</div>
`
: ""}<slot name="toolbar-icon"></slot>
</div>
`
: ""}
${this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="search-toolbar">${headerToolbar}</div>
</slot>
</div>
`
: ""}
<ha-data-table
.hass=${this.hass}
.columns=${this.columns}
.data=${this.data}
.noDataText=${this.noDataText}
.filter=${this.filter}
.selectable=${this.selectable}
.hasFab=${this.hasFab}
.id=${this.id}
.dir=${computeRTLDirection(this.hass)}
.clickable=${this.clickable}
.appendRow=${this.appendRow}
>
${!this.narrow
? html`
<div class="filter-menu">
${this.numHidden || this.activeFilters
? html`<span class="badge"
>${this.numHidden || "!"}</span
>`
: ""}
<slot name="filter-menu"></slot>
<div slot="header">
<slot name="header">
<div class="table-header">${headerToolbar}</div>
</slot>
</div>
`
: ""}<slot name="toolbar-icon"></slot>
</div>
`
: ""}
${this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="search-toolbar">${headerToolbar}</div>
</slot>
</div>
`
: ""}
<ha-data-table
.hass=${this.hass}
.columns=${this.columns}
.data=${this.data}
.filter=${this.filter}
.selectable=${this.selectable}
.hasFab=${this.hasFab}
.id=${this.id}
.noDataText=${this.noDataText}
.dir=${computeRTLDirection(this.hass)}
.clickable=${this.clickable}
.appendRow=${this.appendRow}
>
${!this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="table-header">${headerToolbar}</div>
</slot>
</div>
`
: html` <div slot="header"></div> `}
</ha-data-table>
: html` <div slot="header"></div> `}
</ha-data-table>`}
<div slot="fab"><slot name="fab"></slot></div>
</hass-tabs-subpage>
`;
@@ -374,6 +385,16 @@ export class HaTabsSubpageDataTable extends LitElement {
.filter-menu {
position: relative;
}
.center {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
box-sizing: border-box;
height: 100%;
width: 100%;
padding: 16px;
}
`;
}
}

View File

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

View File

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

View File

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

View File

@@ -1,57 +1,26 @@
import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentPaste,
mdiDrag,
mdiPlus,
} from "@mdi/js";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import { ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation";
import { getService, isService } from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation";
import { Action } from "../../../../data/script";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
import { Entries, HomeAssistant } from "../../../../types";
import { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
import type HaAutomationActionRow from "./ha-automation-action-row";
import { getType } from "./ha-automation-action-row";
import "./types/ha-automation-action-activate_scene";
import "./types/ha-automation-action-choose";
import "./types/ha-automation-action-condition";
import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-event";
import "./types/ha-automation-action-if";
import "./types/ha-automation-action-parallel";
import "./types/ha-automation-action-play_media";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-service";
import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
const PASTE_VALUE = "__paste__";
@customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement {
@@ -150,42 +119,27 @@ export default class HaAutomationAction extends LitElement {
`
)}
</div>
<ha-button-menu
@action=${this._addAction}
.disabled=${this.disabled}
fixed
>
<div class="buttons">
<ha-button
slot="trigger"
outlined
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.add"
)}
@click=${this._addActionDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${this._clipboard?.action
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.paste"
)}
(${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${
getType(this._clipboard.action) || "unknown"
}.label`
)})
<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>
<ha-button
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.add_building_block"
)}
@click=${this._addActionBuildingBlockDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
</div>
`;
}
@@ -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) {
if (this.nested) return;
ev.stopPropagation();
@@ -258,25 +249,6 @@ export default class HaAutomationAction extends LitElement {
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) {
const index = (ev.target as any).index;
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 {
return [
sortableStyles,
@@ -370,6 +326,11 @@ export default class HaAutomationAction extends LitElement {
pointer-events: none;
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 type { HaSelect } from "../../../../../components/ha-select";
import type { Condition } from "../../../../../data/automation";
import { CONDITION_TYPES } from "../../../../../data/condition";
import { CONDITION_ICONS } from "../../../../../data/condition";
import { Entries, HomeAssistant } from "../../../../../types";
import "../../condition/ha-automation-condition-editor";
import type { ActionElement } from "../ha-automation-action-row";
@@ -55,7 +55,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>)
(Object.entries(CONDITION_ICONS) as Entries<typeof CONDITION_ICONS>)
.map(
([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 { Condition, testCondition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n";
import { CONDITION_TYPES } from "../../../../data/condition";
import { CONDITION_ICONS } from "../../../../data/condition";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
@@ -123,7 +123,7 @@ export default class HaAutomationConditionRow extends LitElement {
<h3 slot="header">
<ha-svg-icon
class="condition-icon"
.path=${CONDITION_TYPES[this.condition.condition]}
.path=${CONDITION_ICONS[this.condition.condition]}
></ha-svg-icon>
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)

View File

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

View File

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

View File

@@ -7,11 +7,19 @@ import {
mdiPlay,
mdiPlayCircleOutline,
mdiPlus,
mdiRobotHappy,
mdiStopCircleOutline,
mdiTransitConnection,
} from "@mdi/js";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { differenceInDays } from "date-fns/esm";
@@ -295,6 +303,7 @@ class HaAutomationPicker extends LitElement {
.activeFilters=${this._activeFilters}
.columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._automations(this.automations, this._filteredAutomations)}
.empty=${!this.automations.length}
@row-click=${this._handleRowClicked}
.noDataText=${this.hass.localize(
"ui.panel.config.automation.picker.no_automations"
@@ -318,6 +327,35 @@ class HaAutomationPicker extends LitElement {
@related-changed=${this._relatedFilterChanged}
>
</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
slot="fab"
.label=${this.hass.localize(
@@ -475,9 +513,7 @@ class HaAutomationPicker extends LitElement {
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.picker.learn_more"
)}
${this.hass.localize("ui.panel.config.common.learn_more")}
</a>
</p>
`,
@@ -505,7 +541,16 @@ class HaAutomationPicker extends LitElement {
}
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,
mdiRefresh,
} 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 { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
@@ -41,6 +48,8 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { computeRTL } from "../../../common/util/compute_rtl";
const TABS = ["details", "automation_config", "timeline", "logbook"] as const;
@customElement("ha-automation-trace")
export class HaAutomationTrace extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -67,12 +76,7 @@ export class HaAutomationTrace extends LitElement {
@state() private _logbookEntries?: LogbookEntry[];
@state() private _view:
| "details"
| "config"
| "timeline"
| "logbook"
| "blueprint" = "details";
@state() private _view: (typeof TABS)[number] | "blueprint" = "details";
@query("hat-script-graph") private _graph?: HatScriptGraph;
@@ -213,9 +217,15 @@ export class HaAutomationTrace extends LitElement {
</div>
${this._traces === undefined
? html`<div class="container">Loading…</div>`
? html`<div class="container">
${this.hass!.localize("ui.common.loading")}
</div>`
: 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
? ""
: html`
@@ -230,20 +240,17 @@ export class HaAutomationTrace extends LitElement {
<div class="info">
<div class="tabs top">
${[
["details", "Step Details"],
["timeline", "Trace Timeline"],
["logbook", "Related logbook entries"],
["config", "Automation Config"],
].map(
([view, label]) => html`
${TABS.map(
(view) => html`
<button
tabindex="0"
.view=${view}
class=${classMap({ active: this._view === view })}
@click=${this._showTab}
>
${label}
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.${view}`
)}
</button>
`
)}
@@ -257,7 +264,9 @@ export class HaAutomationTrace extends LitElement {
})}
@click=${this._showTab}
>
Blueprint Config
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.blueprint_config`
)}
</button>
`
: ""}
@@ -265,7 +274,7 @@ export class HaAutomationTrace extends LitElement {
${this._selected === undefined ||
this._logbookEntries === undefined ||
trackedNodes === undefined
? ""
? nothing
: this._view === "details"
? html`
<ha-trace-path-details
@@ -278,7 +287,7 @@ export class HaAutomationTrace extends LitElement {
.renderedNodes=${renderedNodes!}
></ha-trace-path-details>
`
: this._view === "config"
: this._view === "automation_config"
? html`
<ha-trace-config
.hass=${this.hass}

View File

@@ -1,11 +1,13 @@
import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
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 { ensureArray } from "../../../common/array/ensure-array";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-markdown";
import {
Condition,
ManualAutomationConfig,
@@ -83,6 +85,14 @@ export class HaManualAutomationEditor extends LitElement {
></ha-icon-button>
</a>
</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
role="region"
@@ -98,6 +108,9 @@ export class HaManualAutomationEditor extends LitElement {
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header"
)}
<span class="small"
>(${this.hass.localize("ui.common.optional")})</span
>
</h2>
<a
href=${documentationUrl(this.hass, "/docs/automation/condition/")}
@@ -112,6 +125,15 @@ export class HaManualAutomationEditor extends LitElement {
></ha-icon-button>
</a>
</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
role="region"
@@ -143,6 +165,14 @@ export class HaManualAutomationEditor extends LitElement {
</a>
</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
role="region"
@@ -207,9 +237,11 @@ export class HaManualAutomationEditor extends LitElement {
margin: 0;
}
p {
margin-bottom: 0;
margin-top: 0;
}
.header {
margin-top: 16px;
display: flex;
align-items: center;
}
@@ -217,13 +249,18 @@ export class HaManualAutomationEditor extends LitElement {
margin-top: -16px;
}
.header .name {
font-size: 20px;
font-weight: 400;
flex: 1;
margin-bottom: 16px;
}
.header a {
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 { fullEntitiesContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { TRIGGER_TYPES } from "../../../../data/trigger";
import { TRIGGER_ICONS } from "../../../../data/trigger";
import {
showAlertDialog,
showConfirmationDialog,
@@ -150,7 +150,7 @@ export default class HaAutomationTriggerRow extends LitElement {
<h3 slot="header">
<ha-svg-icon
class="trigger-icon"
.path=${TRIGGER_TYPES[this.trigger.platform]}
.path=${TRIGGER_ICONS[this.trigger.platform]}
></ha-svg-icon>
${describeTrigger(this.trigger, this.hass, this._entityReg)}
</h3>

View File

@@ -1,59 +1,25 @@
import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentPaste,
mdiDrag,
mdiPlus,
} from "@mdi/js";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import { AutomationClipboard, Trigger } from "../../../../data/automation";
import { TRIGGER_TYPES } from "../../../../data/trigger";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable";
import { Entries, HomeAssistant } from "../../../../types";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import "./types/ha-automation-trigger-calendar";
import "./types/ha-automation-trigger-conversation";
import "./types/ha-automation-trigger-device";
import "./types/ha-automation-trigger-event";
import "./types/ha-automation-trigger-geo_location";
import "./types/ha-automation-trigger-homeassistant";
import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-persistent_notification";
import "./types/ha-automation-trigger-state";
import "./types/ha-automation-trigger-sun";
import "./types/ha-automation-trigger-tag";
import "./types/ha-automation-trigger-template";
import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
const PASTE_VALUE = "__paste__";
import {
PASTE_VALUE,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
@customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends LitElement {
@@ -147,47 +113,48 @@ export default class HaAutomationTrigger extends LitElement {
</ha-automation-trigger-row>
`
)}
<ha-button-menu
@action=${this._addTrigger}
.disabled=${this.disabled}
fixed
>
<ha-button
slot="trigger"
outlined
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.add"
)}
.disabled=${this.disabled}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</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
outlined
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.add"
)}
</ha-button-menu>
.disabled=${this.disabled}
@click=${this._addTriggerDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
</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) {
super.updated(changedProps);
@@ -261,30 +228,6 @@ export default class HaAutomationTrigger extends LitElement {
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) {
const index = (ev.target as any).index;
const newIndex = index - 1;
@@ -336,22 +279,6 @@ export default class HaAutomationTrigger extends LitElement {
});
}
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(TRIGGER_TYPES) as Entries<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 {
return [
sortableStyles,

View File

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

View File

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

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];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,6 @@ import "../../../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node";
import { showZWaveJSRebuildNetworkRoutesDialog } from "./show-dialog-zwave_js-rebuild-network-routes";
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
@@ -128,358 +127,345 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
.path=${mdiRefresh}
.label=${this.hass!.localize("ui.common.refresh")}
></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._status === "connected" &&
(this._network?.controller.inclusion_state ===
InclusionState.Including ||
this._network?.controller.inclusion_state ===
InclusionState.Excluding)
? html`
<ha-alert alert-type="info">
${this.hass.localize(
`ui.panel.config.zwave_js.common.in_progress_inclusion_exclusion`
${this._network &&
this._status === "connected" &&
(this._network?.controller.inclusion_state ===
InclusionState.Including ||
this._network?.controller.inclusion_state ===
InclusionState.Excluding)
? html`
<ha-alert alert-type="info">
${this.hass.localize(
`ui.panel.config.zwave_js.common.in_progress_inclusion_exclusion`
)}
<mwc-button
slot="action"
.label=${this.hass.localize(
`ui.panel.config.zwave_js.common.cancel_inclusion_exclusion`
)}
<mwc-button
slot="action"
.label=${this.hass.localize(
`ui.panel.config.zwave_js.common.cancel_inclusion_exclusion`
)}
@click=${this._network?.controller.inclusion_state ===
InclusionState.Including
? this._cancelInclusion
: this._cancelExclusion}
>
</mwc-button>
</ha-alert>
`
: ""}
${this._network
? html`
<ha-card class="content network-status">
<div class="card-content">
<div class="heading">
<div class="icon">
${this._status === "disconnected"
? html`<ha-circular-progress
indeterminate
></ha-circular-progress>`
: html`
<ha-svg-icon
.path=${this._icon}
class="network-status-icon ${classMap({
[this._status!]: true,
})}"
slot="item-icon"
></ha-svg-icon>
`}
</div>
${this._status !== "disconnected"
? html`
<div class="details">
${this.hass.localize(
"ui.panel.config.zwave_js.common.network"
)}
${this.hass.localize(
`ui.panel.config.zwave_js.network_status.${this._status}`
)}<br />
<small>
${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.devices`,
{
count:
this._network.controller.nodes.length,
}
)}
${notReadyDevices > 0
? html`(${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.not_ready`,
{ count: notReadyDevices }
)})`
: ""}
</small>
</div>
`
: ``}
</div>
</div>
<div class="card-actions">
<a
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
>
<mwc-button>
${this.hass.localize("ui.panel.config.devices.caption")}
</mwc-button>
</a>
<a
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.entities.caption"
)}
</mwc-button>
</a>
${this._provisioningEntries?.length
? html`<a
href=${`provisioned?config_entry=${this.configEntryId}`}
><mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.provisioned_devices"
)}
</mwc-button></a
>`
: ""}
</div>
</ha-card>
<ha-card header="Diagnostics">
<div class="card-content">
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.driver_version"
)}:
</span>
<span>${this._network.client.driver_version}</span>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_version"
)}:
</span>
<span>${this._network.client.server_version}</span>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.home_id"
)}:
</span>
<span>${this._network.controller.home_id}</span>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_url"
)}:
</span>
<span>${this._network.client.ws_server_url}</span>
</div>
<br />
<ha-expansion-panel
.header=${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.title"
)}
>
<mwc-list noninteractive>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_tx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_tx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_tx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_rx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_rx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_rx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_tx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_tx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_dropped_tx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_rx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_rx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_dropped_rx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.nak.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.nak.tooltip"
)}
</span>
<span slot="meta">${this._statistics?.nak ?? 0}</span>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.can.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.can.tooltip"
)}
</span>
<span slot="meta">${this._statistics?.can ?? 0}</span>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_ack.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_ack.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.timeout_ack ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_response.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_response.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.timeout_response ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_callback.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_callback.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.timeout_callback ?? 0}</span
>
</mwc-list-item>
</mwc-list>
</ha-expansion-panel>
</div>
<div class="card-actions">
<mwc-button
@click=${this._removeNodeClicked}
.disabled=${this._status !== "connected" ||
(this._network?.controller.inclusion_state !==
InclusionState.Idle &&
this._network?.controller.inclusion_state !==
InclusionState.SmartStart)}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.remove_node"
)}
</mwc-button>
<mwc-button
@click=${this._rebuildNetworkRoutesClicked}
.disabled=${this._status === "disconnected"}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.rebuild_network_routes"
)}
</mwc-button>
<mwc-button @click=${this._openOptionFlow}>
${this.hass.localize(
"ui.panel.config.zwave_js.common.reconfigure_server"
)}
</mwc-button>
</div>
</ha-card>
<ha-card>
<div class="card-header">
<h1>Third-Party Data Reporting</h1>
${this._dataCollectionOptIn !== undefined
? html`
<ha-switch
.checked=${this._dataCollectionOptIn === true}
@change=${this._dataCollectionToggled}
></ha-switch>
`
: html`
<ha-circular-progress
size="small"
@click=${this._network?.controller.inclusion_state ===
InclusionState.Including
? this._cancelInclusion
: this._cancelExclusion}
>
</mwc-button>
</ha-alert>
`
: ""}
${this._network
? html`
<ha-card class="content network-status">
<div class="card-content">
<div class="heading">
<div class="icon">
${this._status === "disconnected"
? html`<ha-circular-progress
indeterminate
></ha-circular-progress>
`}
></ha-circular-progress>`
: html`
<ha-svg-icon
.path=${this._icon}
class="network-status-icon ${classMap({
[this._status!]: true,
})}"
slot="item-icon"
></ha-svg-icon>
`}
</div>
${this._status !== "disconnected"
? html`
<div class="details">
Z-Wave
${this.hass.localize(
"ui.panel.config.zwave_js.common.network"
)}
${this.hass.localize(
`ui.panel.config.zwave_js.network_status.${this._status}`
)}<br />
<small>
${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.devices`,
{
count: this._network.controller.nodes.length,
}
)}
${notReadyDevices > 0
? html`(${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.not_ready`,
{ count: notReadyDevices }
)})`
: ""}
</small>
</div>
`
: ``}
</div>
<div class="card-content">
<p>
Enable the reporting of anonymized telemetry and
statistics to the <em>Z-Wave JS organization</em>. This
data will be used to focus development efforts and improve
the user experience. Information about the data that is
collected and how it is used, including an example of the
data collected, can be found in the
<a
target="_blank"
href="https://zwave-js.github.io/node-zwave-js/#/data-collection/data-collection"
>Z-Wave JS data collection documentation</a
>.
</p>
</div>
<div class="card-actions">
<a
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
>
<mwc-button>
${this.hass.localize("ui.panel.config.devices.caption")}
</mwc-button>
</a>
<a
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
>
<mwc-button>
${this.hass.localize("ui.panel.config.entities.caption")}
</mwc-button>
</a>
${this._provisioningEntries?.length
? html`<a
href=${`provisioned?config_entry=${this.configEntryId}`}
><mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.provisioned_devices"
)}
</mwc-button></a
>`
: ""}
</div>
</ha-card>
<ha-card header="Diagnostics">
<div class="card-content">
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.driver_version"
)}:
</span>
<span>${this._network.client.driver_version}</span>
</div>
</ha-card>
`
: ``}
</ha-config-section>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_version"
)}:
</span>
<span>${this._network.client.server_version}</span>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.home_id"
)}:
</span>
<span>${this._network.controller.home_id}</span>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_url"
)}:
</span>
<span>${this._network.client.ws_server_url}</span>
</div>
<br />
<ha-expansion-panel
.header=${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.title"
)}
>
<mwc-list noninteractive>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_tx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_tx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_tx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_rx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_rx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_rx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_tx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_tx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_dropped_tx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_rx.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.messages_dropped_rx.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.messages_dropped_rx ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.nak.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.nak.tooltip"
)}
</span>
<span slot="meta">${this._statistics?.nak ?? 0}</span>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.can.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.can.tooltip"
)}
</span>
<span slot="meta">${this._statistics?.can ?? 0}</span>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_ack.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_ack.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.timeout_ack ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_response.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_response.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.timeout_response ?? 0}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_callback.label"
)}
</span>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.statistics.timeout_callback.tooltip"
)}
</span>
<span slot="meta"
>${this._statistics?.timeout_callback ?? 0}</span
>
</mwc-list-item>
</mwc-list>
</ha-expansion-panel>
</div>
<div class="card-actions">
<mwc-button
@click=${this._removeNodeClicked}
.disabled=${this._status !== "connected" ||
(this._network?.controller.inclusion_state !==
InclusionState.Idle &&
this._network?.controller.inclusion_state !==
InclusionState.SmartStart)}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.remove_node"
)}
</mwc-button>
<mwc-button
@click=${this._rebuildNetworkRoutesClicked}
.disabled=${this._status === "disconnected"}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.rebuild_network_routes"
)}
</mwc-button>
<mwc-button @click=${this._openOptionFlow}>
${this.hass.localize(
"ui.panel.config.zwave_js.common.reconfigure_server"
)}
</mwc-button>
</div>
</ha-card>
<ha-card>
<div class="card-header">
<h1>Third-Party Data Reporting</h1>
${this._dataCollectionOptIn !== undefined
? html`
<ha-switch
.checked=${this._dataCollectionOptIn === true}
@change=${this._dataCollectionToggled}
></ha-switch>
`
: html`
<ha-circular-progress
size="small"
indeterminate
></ha-circular-progress>
`}
</div>
<div class="card-content">
<p>
Enable the reporting of anonymized telemetry and statistics
to the <em>Z-Wave JS organization</em>. This data will be
used to focus development efforts and improve the user
experience. Information about the data that is collected and
how it is used, including an example of the data collected,
can be found in the
<a
target="_blank"
href="https://zwave-js.github.io/node-zwave-js/#/data-collection/data-collection"
>Z-Wave JS data collection documentation</a
>.
</p>
</div>
</ha-card>
`
: ``}
<ha-fab
slot="fab"
.label=${this.hass.localize(

View File

@@ -3,12 +3,20 @@ import {
mdiDelete,
mdiHelpCircle,
mdiInformationOutline,
mdiPalette,
mdiPencilOff,
mdiPlay,
mdiPlus,
} from "@mdi/js";
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 memoizeOne from "memoize-one";
import { differenceInDays } from "date-fns/esm";
@@ -21,6 +29,7 @@ import {
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button-related-filter-menu";
import "../../../components/ha-fab";
import "../../../components/ha-button";
import "../../../components/ha-icon-button";
import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
@@ -214,6 +223,7 @@ class HaSceneDashboard extends LitElement {
.columns=${this._columns(this.hass.locale, this.narrow)}
id="entity_id"
.data=${this._scenes(this.scenes, this._filteredScenes)}
.empty=${!this.scenes.length}
.activeFilters=${this._activeFilters}
.noDataText=${this.hass.localize(
"ui.panel.config.scene.picker.no_scenes"
@@ -238,6 +248,30 @@ class HaSceneDashboard extends LitElement {
@related-changed=${this._relatedFilterChanged}
>
</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">
<ha-fab
.label=${this.hass.localize(
@@ -336,7 +370,7 @@ class HaSceneDashboard extends LitElement {
target="_blank"
rel="noreferrer"
>
${this.hass.localize("ui.panel.config.scene.picker.learn_more")}
${this.hass.localize("ui.panel.config.common.learn_more")}
</a>
</p>
`,
@@ -350,6 +384,11 @@ class HaSceneDashboard extends LitElement {
a {
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
? ""
: html`${this.hass.localize(
`ui.panel.config.automation.editor.${key}s.header`
`ui.panel.config.automation.editor.${key}s.name`
)}:
${value.error}<br />`
);

View File

@@ -5,10 +5,18 @@ import {
mdiInformationOutline,
mdiPlay,
mdiPlus,
mdiScriptText,
mdiTransitConnection,
} from "@mdi/js";
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 { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
@@ -241,6 +249,7 @@ class HaScriptPicker extends LitElement {
.tabs=${configSections.automations}
.columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._scripts(this.scripts, this._filteredScripts)}
.empty=${!this.scripts.length}
.activeFilters=${this._activeFilters}
id="entity_id"
.noDataText=${this.hass.localize(
@@ -266,6 +275,32 @@ class HaScriptPicker extends LitElement {
@related-changed=${this._relatedFilterChanged}
>
</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
slot="fab"
?is-wide=${this.isWide}
@@ -385,7 +420,7 @@ class HaScriptPicker extends LitElement {
target="_blank"
rel="noreferrer"
>
${this.hass.localize("ui.panel.config.script.picker.learn_more")}
${this.hass.localize("ui.panel.config.common.learn_more")}
</a>
</p>
`,
@@ -471,6 +506,11 @@ class HaScriptPicker extends LitElement {
a {
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 { stopPropagation } from "../../../common/dom/stop_propagation";
import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown";
import "../../../components/ha-select";
import {
extractApiErrorMessage,

View File

@@ -1,17 +1,11 @@
import "@material/mwc-button";
import {
css,
CSSResultGroup,
html,
LitElement,
TemplateResult,
nothing,
} from "lit";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield";
import "../../../components/ha-qr-code";
import "../../../components/ha-switch";
import "../../../components/ha-textfield";
import { Tag, UpdateTagParams } from "../../../data/tag";
@@ -20,8 +14,6 @@ import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { TagDetailDialogParams } from "./show-dialog-tag-detail";
const QR_LOGO_URL = "/static/icons/favicon-192x192.png";
@customElement("dialog-tag-detail")
class DialogTagDetail
extends LitElement
@@ -39,8 +31,6 @@ class DialogTagDetail
@state() private _submitting = false;
@state() private _qrCode?: TemplateResult;
public showDialog(params: TagDetailDialogParams): void {
this._params = params;
this._error = undefined;
@@ -50,13 +40,10 @@ class DialogTagDetail
this._id = "";
this._name = "";
}
this._generateQR();
}
public closeDialog(): void {
this._params = undefined;
this._qrCode = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -130,9 +117,15 @@ class DialogTagDetail
})}
</p>
</div>
${this._qrCode
? html` <div id="qr">${this._qrCode}</div> `
: ""}
<div id="qr">
<ha-qr-code
.data=${this._params!.entry!.id}
center-image="/static/icons/favicon-192x192.png"
error-correction-level="quartile"
scale="5"
>
</ha-qr-code>
</div>
`
: ``}
</div>
@@ -158,7 +151,7 @@ class DialogTagDetail
: this.hass!.localize("ui.panel.config.tag.detail.create")}
</mwc-button>
${this._params.openWrite && !this._params.entry
? html` <mwc-button
? html`<mwc-button
slot="primaryAction"
@click=${this._updateWriteEntry}
.disabled=${this._submitting || !this._name}
@@ -221,41 +214,6 @@ class DialogTagDetail
}
}
private async _generateQR() {
const qrcode = await import("qrcode");
const canvas = await qrcode.toCanvas(
`https://www.home-assistant.io/tag/${this._params!.entry!.id}`,
{
width: 180,
errorCorrectionLevel: "Q",
color: {
light: "#fff",
},
}
);
const context = canvas.getContext("2d");
const imageObj = new Image();
imageObj.src = QR_LOGO_URL;
await new Promise((resolve) => {
imageObj.onload = resolve;
});
context?.drawImage(
imageObj,
canvas.width / 3,
canvas.height / 3,
canvas.width / 3,
canvas.height / 3
);
this._qrCode = html`<img
alt=${this.hass.localize("ui.panel.config.tag.qr_code_image", {
name: this._name,
})}
src=${canvas.toDataURL()}
></img>`;
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
@@ -270,6 +228,9 @@ class DialogTagDetail
display: block;
margin: 8px 0;
}
::slotted(img) {
height: 100%;
}
`,
];
}

View File

@@ -110,7 +110,10 @@ export class AssistPipelineDetailWakeWord extends LitElement {
`ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.wakeword.no_wake_words`
)}
<a
href=${documentationUrl(this.hass, "/docs/assist/")}
href=${documentationUrl(
this.hass,
"/voice_control/install_wake_word_add_on/"
)}
target="_blank"
rel="noreferrer noopener"
>${this.hass.localize(

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