mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-19 01:49:45 +00:00
Compare commits
98 Commits
dashboard_
...
refactor_f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
58e968cf62 | ||
![]() |
fe5431976d | ||
![]() |
72dedc7596 | ||
![]() |
8615b799c6 | ||
![]() |
fd6cae48b7 | ||
![]() |
ecadc06b45 | ||
![]() |
e5d31d85dd | ||
![]() |
e427ffca5f | ||
![]() |
4f05bd9e22 | ||
![]() |
0d8c0ac1a0 | ||
![]() |
3984702d3e | ||
![]() |
979085a9de | ||
![]() |
7a6fc573c2 | ||
![]() |
6929a9c603 | ||
![]() |
41bb677ebe | ||
![]() |
8368fb4408 | ||
![]() |
acbb7e0453 | ||
![]() |
6224cb6994 | ||
![]() |
29fefa1d60 | ||
![]() |
c7a98fa5a1 | ||
![]() |
46565715c9 | ||
![]() |
8a8b04ed00 | ||
![]() |
e721481757 | ||
![]() |
2933d642ed | ||
![]() |
7d412a9913 | ||
![]() |
5eaa6dafd9 | ||
![]() |
f994b378f1 | ||
![]() |
7d6dfaad15 | ||
![]() |
def2eadc24 | ||
![]() |
53a25dde26 | ||
![]() |
6f7da16d1d | ||
![]() |
5ac0360494 | ||
![]() |
1b6def4875 | ||
![]() |
52fcf64cfd | ||
![]() |
c650e23432 | ||
![]() |
428604d91d | ||
![]() |
d0c1481f76 | ||
![]() |
6cc9a99b77 | ||
![]() |
d25f49b694 | ||
![]() |
b92d25e28a | ||
![]() |
bf5c5bc46f | ||
![]() |
e7cc842be4 | ||
![]() |
cb29d35949 | ||
![]() |
fe18f70e51 | ||
![]() |
ee57f26415 | ||
![]() |
721ec8e559 | ||
![]() |
c584f83071 | ||
![]() |
3c2fed5041 | ||
![]() |
7e6d974438 | ||
![]() |
c95a30c837 | ||
![]() |
cb568d005e | ||
![]() |
442cce1574 | ||
![]() |
c25baf25ad | ||
![]() |
5e279405c7 | ||
![]() |
e5707b423f | ||
![]() |
7983556f98 | ||
![]() |
1916dff57b | ||
![]() |
b11563d618 | ||
![]() |
b180a587bf | ||
![]() |
5b11e0ce29 | ||
![]() |
d6684c5806 | ||
![]() |
1a0e3890f4 | ||
![]() |
caece9d6ad | ||
![]() |
953a3793c4 | ||
![]() |
8bfae3b4cf | ||
![]() |
6911685bd0 | ||
![]() |
71025eaf4d | ||
![]() |
3aa612b766 | ||
![]() |
a2ffd0ae83 | ||
![]() |
c399da586e | ||
![]() |
d7826e4e6c | ||
![]() |
f5d13c9079 | ||
![]() |
01a142790f | ||
![]() |
df54687de1 | ||
![]() |
bded31b311 | ||
![]() |
67e573aff7 | ||
![]() |
6295c4ac76 | ||
![]() |
469811847f | ||
![]() |
8a1aefefca | ||
![]() |
bea16028a1 | ||
![]() |
bedb7d1d9e | ||
![]() |
1a3a20f478 | ||
![]() |
68ecb7c219 | ||
![]() |
61b04a882b | ||
![]() |
f8e621c5b9 | ||
![]() |
2f2209682e | ||
![]() |
f4f361b51a | ||
![]() |
55e59f8cb0 | ||
![]() |
6d20ed0a22 | ||
![]() |
874f604295 | ||
![]() |
30d36a11c1 | ||
![]() |
09dcc29175 | ||
![]() |
8f07e6f141 | ||
![]() |
7b6b5724e1 | ||
![]() |
521c0b58c8 | ||
![]() |
53839ab7b1 | ||
![]() |
dcfe9617b3 | ||
![]() |
58eebf2dbd |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -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
|
||||
|
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -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
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -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
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -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
|
||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -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
|
||||
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -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
|
||||
|
@@ -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 }],
|
||||
|
@@ -426,6 +426,7 @@ gulp.task(
|
||||
"fetch-nightly-translations",
|
||||
gulp.series("clean-translations", "ensure-translations-build-dir")
|
||||
),
|
||||
gulp.parallel("create-test-metadata", "create-test-translation"),
|
||||
"build-master-translation",
|
||||
"build-merged-translations",
|
||||
"build-translation-fragment-supervisor",
|
||||
|
@@ -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",
|
||||
|
@@ -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: `
|
||||
|
@@ -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"],
|
||||
|
@@ -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,
|
||||
}),
|
||||
];
|
||||
|
||||
|
@@ -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,
|
||||
|
44
package.json
44
package.json
@@ -25,7 +25,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.23.6",
|
||||
"@babel/runtime": "7.23.7",
|
||||
"@braintree/sanitize-url": "7.0.0",
|
||||
"@codemirror/autocomplete": "6.11.1",
|
||||
"@codemirror/commands": "6.3.2",
|
||||
@@ -81,8 +81,8 @@
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "=1.1.1",
|
||||
"@mdi/js": "7.3.67",
|
||||
"@mdi/svg": "7.3.67",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@polymer/paper-input": "3.2.1",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
"@polymer/paper-listbox": "3.0.1",
|
||||
@@ -90,8 +90,8 @@
|
||||
"@polymer/paper-toast": "3.0.1",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.3.1",
|
||||
"@vaadin/vaadin-themable-mixin": "24.3.1",
|
||||
"@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.1.0",
|
||||
"marked": "11.1.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@@ -138,7 +138,7 @@
|
||||
"unfetch": "5.0.0",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-network": "9.1.9",
|
||||
"vue": "2.7.15",
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.0.0",
|
||||
@@ -150,13 +150,13 @@
|
||||
"xss": "1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.23.6",
|
||||
"@babel/core": "7.23.7",
|
||||
"@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/plugin-proposal-decorators": "7.23.7",
|
||||
"@babel/plugin-transform-runtime": "7.23.7",
|
||||
"@babel/preset-env": "7.23.7",
|
||||
"@babel/preset-typescript": "7.23.3",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.8.3",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.8.4",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.1.0",
|
||||
"@octokit/auth-oauth-device": "6.0.1",
|
||||
@@ -184,13 +184,13 @@
|
||||
"@types/tar": "6.1.10",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "6.14.0",
|
||||
"@typescript-eslint/parser": "6.14.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.16.0",
|
||||
"@typescript-eslint/parser": "6.16.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"chai": "4.3.10",
|
||||
"chai": "5.0.0",
|
||||
"del": "7.1.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
@@ -199,7 +199,7 @@
|
||||
"eslint-import-resolver-webpack": "0.13.8",
|
||||
"eslint-plugin-disable": "2.0.3",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-lit": "1.10.1",
|
||||
"eslint-plugin-lit": "1.11.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.1",
|
||||
"eslint-plugin-unused-imports": "3.0.0",
|
||||
"eslint-plugin-wc": "2.0.4",
|
||||
@@ -223,19 +223,19 @@
|
||||
"map-stream": "0.0.7",
|
||||
"mocha": "10.2.0",
|
||||
"object-hash": "3.0.0",
|
||||
"open": "9.1.0",
|
||||
"open": "10.0.2",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.1.1",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.11.0",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "17.0.1",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.2",
|
||||
"tar": "6.2.0",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"ts-lit-plugin": "2.0.1",
|
||||
"typescript": "5.3.3",
|
||||
"vinyl-buffer": "1.0.1",
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20231208.2"
|
||||
version = "20240104.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -1,7 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const util = require("util");
|
||||
const exec = util.promisify(require("child_process").exec);
|
||||
/* eslint-disable no-console */
|
||||
import fs from "fs";
|
||||
import util from "util";
|
||||
import child_process from "child_process";
|
||||
|
||||
const exec = util.promisify(child_process.exec);
|
||||
|
||||
function patch(version) {
|
||||
const parts = version.split(".");
|
||||
@@ -18,7 +21,7 @@ function today() {
|
||||
|
||||
function auto(version) {
|
||||
const todayVersion = today();
|
||||
if (todayVersion !== version) {
|
||||
if (todayVersion.split(".")[0] !== version.split(".")[0]) {
|
||||
return todayVersion;
|
||||
}
|
||||
return patch(version);
|
||||
@@ -44,7 +47,7 @@ async function main(args) {
|
||||
commit = true;
|
||||
} else {
|
||||
method = args.length > 0 && methods[args[0]];
|
||||
commit = args.length > 1 && args[1] == "--commit";
|
||||
commit = args.length > 1 && args[1] === "--commit";
|
||||
}
|
||||
|
||||
if (!method) {
|
@@ -47,7 +47,7 @@ export class HaAuthTextField extends HaTextField {
|
||||
// TODO: live() directive needs casting for lit-analyzer
|
||||
// https://github.com/runem/lit-analyzer/pull/91/files
|
||||
// TODO: lit-analyzer labels min/max as (number|string) instead of string
|
||||
return html` <input
|
||||
return html`<input
|
||||
aria-labelledby=${ifDefined(ariaLabelledbyOrUndef)}
|
||||
aria-controls=${ifDefined(ariaControlsOrUndef)}
|
||||
aria-describedby=${ifDefined(ariaDescribedbyOrUndef)}
|
||||
|
@@ -29,6 +29,7 @@ import {
|
||||
mdiFlash,
|
||||
mdiFlower,
|
||||
mdiFormatListBulleted,
|
||||
mdiFormatListCheckbox,
|
||||
mdiFormTextbox,
|
||||
mdiGauge,
|
||||
mdiGoogleAssistant,
|
||||
@@ -64,6 +65,7 @@ import {
|
||||
mdiTransmissionTower,
|
||||
mdiWater,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherPartlyCloudy,
|
||||
mdiWeatherPouring,
|
||||
mdiWeatherRainy,
|
||||
mdiWeatherWindy,
|
||||
@@ -128,6 +130,7 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
updater: mdiCloudUpload,
|
||||
vacuum: mdiRobotVacuum,
|
||||
wake_word: mdiChatSleep,
|
||||
weather: mdiWeatherPartlyCloudy,
|
||||
zone: mdiMapMarkerRadius,
|
||||
};
|
||||
|
||||
@@ -166,6 +169,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
precipitation_intensity: mdiWeatherPouring,
|
||||
pressure: mdiGauge,
|
||||
reactive_power: mdiFlash,
|
||||
shopping_List: mdiFormatListCheckbox,
|
||||
signal_strength: mdiWifi,
|
||||
sound_pressure: mdiEarHearing,
|
||||
speed: mdiSpeedometer,
|
||||
@@ -250,6 +254,7 @@ export const DOMAINS_INPUT_ROW = [
|
||||
"text",
|
||||
"time",
|
||||
"vacuum",
|
||||
"valve",
|
||||
];
|
||||
|
||||
/** States that we consider "off". */
|
||||
@@ -268,6 +273,7 @@ export const DOMAINS_TOGGLE = new Set([
|
||||
"group",
|
||||
"automation",
|
||||
"humidifier",
|
||||
"valve",
|
||||
]);
|
||||
|
||||
/** Domains that have a dynamic entity image / picture. */
|
||||
|
@@ -50,6 +50,7 @@ export const FIXED_DOMAIN_STATES = {
|
||||
timer: ["active", "idle", "paused"],
|
||||
update: ["on", "off"],
|
||||
vacuum: ["cleaning", "docked", "error", "idle", "paused", "returning"],
|
||||
valve: ["closed", "closing", "open", "opening"],
|
||||
weather: [
|
||||
"clear-night",
|
||||
"cloudy",
|
||||
|
@@ -446,6 +446,7 @@ export class HaAreaPicker extends LitElement {
|
||||
cancel: () => {
|
||||
this._setValue(undefined);
|
||||
this._suggestion = undefined;
|
||||
this.comboBox.setInputValue("");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -18,7 +18,8 @@ export interface datePickerDialogParams {
|
||||
max?: string;
|
||||
locale?: string;
|
||||
firstWeekday?: number;
|
||||
onChange: (value: string) => void;
|
||||
canClear?: boolean;
|
||||
onChange: (value: string | undefined) => void;
|
||||
}
|
||||
|
||||
const showDatePickerDialog = (
|
||||
@@ -49,6 +50,8 @@ export class HaDateInput extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public canClear?: boolean;
|
||||
|
||||
render() {
|
||||
return html`<ha-textfield
|
||||
.label=${this.label}
|
||||
@@ -58,6 +61,7 @@ export class HaDateInput extends LitElement {
|
||||
helperPersistent
|
||||
readonly
|
||||
@click=${this._openDialog}
|
||||
@keydown=${this._keyDown}
|
||||
.value=${this.value
|
||||
? formatDateNumeric(
|
||||
new Date(`${this.value.split("T")[0]}T00:00:00`),
|
||||
@@ -82,13 +86,23 @@ export class HaDateInput extends LitElement {
|
||||
min: this.min || "1970-01-01",
|
||||
max: this.max,
|
||||
value: this.value,
|
||||
canClear: this.canClear,
|
||||
onChange: (value) => this._valueChanged(value),
|
||||
locale: this.locale.language,
|
||||
firstWeekday: firstWeekdayIndex(this.locale),
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(value: string) {
|
||||
private _keyDown(ev: KeyboardEvent) {
|
||||
if (!this.canClear) {
|
||||
return;
|
||||
}
|
||||
if (["Backspace", "Delete"].includes(ev.key)) {
|
||||
this._valueChanged(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(value: string | undefined) {
|
||||
if (this.value !== value) {
|
||||
this.value = value;
|
||||
fireEvent(this, "change");
|
||||
|
@@ -50,6 +50,15 @@ export class HaDialogDatePicker extends LitElement {
|
||||
@datepicker-value-updated=${this._valueChanged}
|
||||
.firstDayOfWeek=${this._params.firstWeekday}
|
||||
></app-datepicker>
|
||||
${this._params.canClear
|
||||
? html`<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._clear}
|
||||
class="warning"
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.clear")}
|
||||
</mwc-button>`
|
||||
: nothing}
|
||||
<mwc-button slot="secondaryAction" @click=${this._setToday}>
|
||||
${this.hass.localize("ui.dialogs.date-picker.today")}
|
||||
</mwc-button>
|
||||
@@ -66,6 +75,11 @@ export class HaDialogDatePicker extends LitElement {
|
||||
this._value = ev.detail.value;
|
||||
}
|
||||
|
||||
private _clear() {
|
||||
this._params?.onChange(undefined);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _setToday() {
|
||||
const today = new Date();
|
||||
this._value = format(today, "yyyy-MM-dd");
|
||||
|
@@ -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 {
|
||||
|
@@ -10,9 +10,9 @@ class HaLabeledSlider extends LitElement {
|
||||
|
||||
@property() public caption?: string;
|
||||
|
||||
@property() public disabled?: boolean;
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public required?: boolean;
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property() public min: number = 0;
|
||||
|
||||
|
@@ -36,16 +36,24 @@ export class HaListItem extends ListItemBase {
|
||||
--mdc-list-item-graphic-margin,
|
||||
16px
|
||||
) !important;
|
||||
direction: var(--direction);
|
||||
direction: var(--direction) !important;
|
||||
}
|
||||
span.material-icons:last-of-type {
|
||||
margin-inline-start: auto !important;
|
||||
margin-inline-end: 0px !important;
|
||||
direction: var(--direction);
|
||||
direction: var(--direction) !important;
|
||||
}
|
||||
.mdc-deprecated-list-item__meta {
|
||||
display: var(--mdc-list-item-meta-display);
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
:host([graphic="icon"]:not([twoline]))
|
||||
.mdc-deprecated-list-item__graphic {
|
||||
margin-inline-end: var(
|
||||
--mdc-list-item-graphic-margin,
|
||||
20px
|
||||
) !important;
|
||||
}
|
||||
:host([multiline-secondary]) {
|
||||
height: auto;
|
||||
@@ -78,6 +86,15 @@ export class HaListItem extends ListItemBase {
|
||||
pointer-events: unset;
|
||||
}
|
||||
`,
|
||||
// safari workaround - must be explicit
|
||||
document.dir === "rtl"
|
||||
? css`
|
||||
span.material-icons:first-of-type,
|
||||
span.material-icons:last-of-type {
|
||||
direction: rtl !important;
|
||||
}
|
||||
`
|
||||
: css``,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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?;
|
||||
|
114
src/components/ha-qr-code.ts
Normal file
114
src/components/ha-qr-code.ts
Normal 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;
|
||||
}
|
||||
`;
|
||||
}
|
@@ -4,7 +4,14 @@ import {
|
||||
HassServices,
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
@@ -83,6 +90,8 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced?: boolean;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public hidePicker?: boolean;
|
||||
|
||||
@state() private _value!: this["value"];
|
||||
|
||||
@state() private _checkedKeys = new Set();
|
||||
@@ -363,12 +372,14 @@ export class HaServiceControl extends LitElement {
|
||||
)) ||
|
||||
serviceData?.description;
|
||||
|
||||
return html`<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._value?.service}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-picker>
|
||||
return html`${this.hidePicker
|
||||
? nothing
|
||||
: html`<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._value?.service}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-picker>`}
|
||||
<div class="description">
|
||||
${description ? html`<p>${description}</p>` : ""}
|
||||
${this._manifest
|
||||
@@ -735,6 +746,9 @@ export class HaServiceControl extends LitElement {
|
||||
margin: var(--service-control-padding, 0 16px);
|
||||
padding: 16px 0;
|
||||
}
|
||||
:host([hidePicker]) p {
|
||||
padding-top: 0;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
width: 32px;
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -62,7 +62,7 @@ class BrowseMediaTTS extends LitElement {
|
||||
this.hass.localize(
|
||||
"ui.components.media-browser.tts.example_message",
|
||||
{
|
||||
name: this.hass.user?.name || "",
|
||||
name: this.hass.user?.name || "Alice",
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
@@ -53,7 +53,7 @@ class MediaUploadButton extends LitElement {
|
||||
${this._uploading > 0
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
size="tiny"
|
||||
size="small"
|
||||
indeterminate
|
||||
area-label="Uploading"
|
||||
slot="icon"
|
||||
|
@@ -133,7 +133,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
|
||||
if (result?.enabled === false) {
|
||||
return html`${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.disabled_node"
|
||||
"ui.panel.config.automation.trace.path.disabled_step"
|
||||
)}`;
|
||||
}
|
||||
|
||||
@@ -208,11 +208,19 @@ export class HaTracePathDetails extends LitElement {
|
||||
const paths = this.trace.trace;
|
||||
const data: ActionTraceStep[] = paths[this.selected.path];
|
||||
|
||||
if (data === undefined) {
|
||||
return html`<div class="padded-box">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.step_not_executed"
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="padded-box">
|
||||
${data.map(
|
||||
(trace, idx) => html`
|
||||
${idx > 0
|
||||
${data.length > 1
|
||||
? html`<p>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.iteration",
|
||||
@@ -240,7 +248,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
if (index === -1) {
|
||||
return html`<div class="padded-box">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.node_not_tracked"
|
||||
"ui.panel.config.automation.trace.path.step_not_executed"
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
@@ -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,44 @@ 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: {},
|
||||
service: {},
|
||||
},
|
||||
},
|
||||
} 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);
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -19,14 +19,14 @@ export const enum CoverEntityFeature {
|
||||
}
|
||||
|
||||
export function isFullyOpen(stateObj: CoverEntity) {
|
||||
if (stateObj.attributes.current_position !== undefined) {
|
||||
if (stateObj.attributes.current_position != null) {
|
||||
return stateObj.attributes.current_position === 100;
|
||||
}
|
||||
return stateObj.state === "open";
|
||||
}
|
||||
|
||||
export function isFullyClosed(stateObj: CoverEntity) {
|
||||
if (stateObj.attributes.current_position !== undefined) {
|
||||
if (stateObj.attributes.current_position != null) {
|
||||
return stateObj.attributes.current_position === 0;
|
||||
}
|
||||
return stateObj.state === "closed";
|
||||
|
@@ -16,7 +16,9 @@ export type IntegrationType =
|
||||
| "helper"
|
||||
| "hub"
|
||||
| "service"
|
||||
| "hardware";
|
||||
| "hardware"
|
||||
| "entity"
|
||||
| "system";
|
||||
|
||||
export interface IntegrationManifest {
|
||||
is_built_in: boolean;
|
||||
|
@@ -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,
|
||||
|
@@ -52,6 +52,7 @@ export const serviceActionStruct: Describe<ServiceAction> = assign(
|
||||
target: optional(targetStruct),
|
||||
data: optional(object()),
|
||||
response_variable: optional(string()),
|
||||
metadata: optional(object()),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -133,6 +134,7 @@ export interface ServiceAction extends BaseAction {
|
||||
target?: HassServiceTarget;
|
||||
data?: Record<string, unknown>;
|
||||
response_variable?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DeviceAction extends BaseAction {
|
||||
|
@@ -168,6 +168,18 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
const service =
|
||||
hass.localize(`component.${domain}.services.${serviceName}.name`) ||
|
||||
hass.services[domain][serviceName]?.name;
|
||||
|
||||
if (config.metadata) {
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.service_name`,
|
||||
{
|
||||
domain: domainToName(hass.localize, domain),
|
||||
name: service || config.service,
|
||||
targets: formatListWithAnds(hass.locale, targets),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.service_based_on_name`,
|
||||
{
|
||||
@@ -404,7 +416,9 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
if (actionType === "device_action") {
|
||||
const config = action as DeviceAction;
|
||||
if (!config.device_id) {
|
||||
return "Device action";
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.device_id.description.no_device`
|
||||
);
|
||||
}
|
||||
const localized = localizeDeviceAutomationAction(
|
||||
hass,
|
||||
|
@@ -18,6 +18,8 @@ export interface TodoItem {
|
||||
uid: string;
|
||||
summary: string;
|
||||
status: TodoItemStatus;
|
||||
description?: string | null;
|
||||
due?: string | null;
|
||||
}
|
||||
|
||||
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,36 @@ 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,
|
||||
due_datetime: item.due?.includes("T") ? item.due : undefined,
|
||||
due_date:
|
||||
item.due === undefined || item.due?.includes("T")
|
||||
? undefined
|
||||
: item.due,
|
||||
},
|
||||
{ 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 === undefined || item.due?.includes("T")
|
||||
? undefined
|
||||
: item.due,
|
||||
},
|
||||
{ entity_id }
|
||||
);
|
||||
|
@@ -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;
|
||||
|
@@ -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", {
|
||||
|
@@ -14,14 +14,14 @@ export const enum ValveEntityFeature {
|
||||
}
|
||||
|
||||
export function isFullyOpen(stateObj: ValveEntity) {
|
||||
if (stateObj.attributes.current_position !== undefined) {
|
||||
if (stateObj.attributes.current_position != null) {
|
||||
return stateObj.attributes.current_position === 100;
|
||||
}
|
||||
return stateObj.state === "open";
|
||||
}
|
||||
|
||||
export function isFullyClosed(stateObj: ValveEntity) {
|
||||
if (stateObj.attributes.current_position !== undefined) {
|
||||
if (stateObj.attributes.current_position != null) {
|
||||
return stateObj.attributes.current_position === 0;
|
||||
}
|
||||
return stateObj.state === "closed";
|
||||
|
@@ -207,7 +207,8 @@ export class DialogAreaFilter
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
.handle {
|
||||
cursor: move;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
|
@@ -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"
|
||||
|
@@ -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;
|
||||
|
92
src/dialogs/update_backup/dialog-update-backup.ts
Normal file
92
src/dialogs/update_backup/dialog-update-backup.ts
Normal 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;
|
||||
}
|
||||
}
|
35
src/dialogs/update_backup/show-update-backup-dialog.ts
Normal file
35
src/dialogs/update_backup/show-update-backup-dialog.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
@@ -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;
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
this.hass.localize(
|
||||
`ui.components.calendar.event.${isCreate ? "add" : "edit"}`
|
||||
)
|
||||
)}
|
||||
>
|
||||
<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;
|
||||
|
@@ -29,6 +29,8 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { domainIconWithoutDefault } from "../../../../common/entity/domain_icon";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-alert";
|
||||
@@ -37,7 +39,7 @@ import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { ACTION_TYPES, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
|
||||
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
|
||||
import { AutomationClipboard } from "../../../../data/automation";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
@@ -82,9 +84,9 @@ export const getType = (action: Action | undefined) => {
|
||||
if (["and", "or", "not"].some((key) => key in action)) {
|
||||
return "condition" as const;
|
||||
}
|
||||
return Object.keys(ACTION_TYPES).find(
|
||||
return Object.keys(ACTION_ICONS).find(
|
||||
(option) => option in action
|
||||
) as keyof typeof ACTION_TYPES;
|
||||
) as keyof typeof ACTION_ICONS;
|
||||
};
|
||||
|
||||
export interface ActionElement extends LitElement {
|
||||
@@ -190,7 +192,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
<h3 slot="header">
|
||||
<ha-svg-icon
|
||||
class="action-icon"
|
||||
.path=${ACTION_TYPES[type!]}
|
||||
.path=${type === "service" &&
|
||||
"service" in this.action &&
|
||||
this.action.service
|
||||
? domainIconWithoutDefault(
|
||||
computeDomain(this.action.service as string)
|
||||
) || ACTION_ICONS[type!]
|
||||
: ACTION_ICONS[type!]}
|
||||
></ha-svg-icon>
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
|
@@ -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,44 @@ export default class HaAutomationAction extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _addActionDialog() {
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "action",
|
||||
add: this._addAction,
|
||||
clipboardItem: getType(this._clipboard?.action),
|
||||
});
|
||||
}
|
||||
|
||||
private _addActionBuildingBlockDialog() {
|
||||
showAddAutomationElementDialog(this, {
|
||||
type: "action",
|
||||
add: this._addAction,
|
||||
clipboardItem: getType(this._clipboard?.action),
|
||||
group: "building_blocks",
|
||||
});
|
||||
}
|
||||
|
||||
private _addAction = (action: string) => {
|
||||
let actions: Action[];
|
||||
if (action === PASTE_VALUE) {
|
||||
actions = this.actions.concat(deepClone(this._clipboard!.action));
|
||||
} else if (isService(action)) {
|
||||
actions = this.actions.concat({
|
||||
service: getService(action),
|
||||
metadata: {},
|
||||
});
|
||||
} else {
|
||||
const elClass = customElements.get(
|
||||
`ha-automation-action-${action}`
|
||||
) as CustomElementConstructor & { defaultConfig: Action };
|
||||
actions = this.actions.concat(
|
||||
elClass ? { ...elClass.defaultConfig } : { [action]: {} }
|
||||
);
|
||||
}
|
||||
this._focusLastActionOnChange = true;
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
};
|
||||
|
||||
private async _enterReOrderMode(ev: CustomEvent) {
|
||||
if (this.nested) return;
|
||||
ev.stopPropagation();
|
||||
@@ -258,25 +250,6 @@ export default class HaAutomationAction extends LitElement {
|
||||
return this._actionKeys.get(action)!;
|
||||
}
|
||||
|
||||
private _addAction(ev: CustomEvent<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 +301,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,
|
||||
@@ -363,13 +320,19 @@ export default class HaAutomationAction extends LitElement {
|
||||
overflow: hidden;
|
||||
}
|
||||
.handle {
|
||||
cursor: move;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
padding: 12px;
|
||||
}
|
||||
.handle ha-svg-icon {
|
||||
pointer-events: none;
|
||||
height: 24px;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -543,7 +543,8 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
.handle {
|
||||
cursor: move;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
padding: 12px;
|
||||
}
|
||||
.handle ha-svg-icon {
|
||||
|
@@ -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]) =>
|
||||
[
|
||||
|
@@ -9,6 +9,7 @@ import type { HomeAssistant } from "../../../../../types";
|
||||
import "../ha-automation-action";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
import { isTemplate } from "../../../../../common/string/has-template";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
@@ -32,7 +33,12 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, type: string, reOrderMode: boolean) =>
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
type: string,
|
||||
reOrderMode: boolean,
|
||||
template: boolean
|
||||
) =>
|
||||
[
|
||||
{
|
||||
name: "type",
|
||||
@@ -53,7 +59,9 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
{
|
||||
name: "count",
|
||||
required: true,
|
||||
selector: { number: { mode: "box", min: 1 } },
|
||||
selector: template
|
||||
? ({ template: {} } as const)
|
||||
: ({ number: { mode: "box", min: 1 } } as const),
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
@@ -89,10 +97,13 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
type ?? "count",
|
||||
this.reOrderMode
|
||||
this.reOrderMode,
|
||||
"count" in action && typeof action.count === "string"
|
||||
? isTemplate(action.count)
|
||||
: false
|
||||
);
|
||||
const data = { ...action, type };
|
||||
return html` <ha-form
|
||||
return html`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
|
674
src/panels/config/automation/add-automation-element-dialog.ts
Normal file
674
src/panels/config/automation/add-automation-element-dialog.ts
Normal file
@@ -0,0 +1,674 @@
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
|
||||
import Fuse, { IFuseOptions } from "fuse.js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
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 { domainIconWithoutDefault } 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 { brandsUrl } from "../../../util/brands-url";
|
||||
import {
|
||||
AddAutomationElementDialogParams,
|
||||
PASTE_VALUE,
|
||||
} from "./show-add-automation-element-dialog";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
|
||||
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;
|
||||
image?: 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",
|
||||
]);
|
||||
|
||||
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
|
||||
|
||||
@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;
|
||||
|
||||
@state() private _domains?: Set<string>;
|
||||
|
||||
@query("ha-dialog") private _dialog?: HaDialog;
|
||||
|
||||
private _fullScreen = false;
|
||||
|
||||
@state() private _width?: number;
|
||||
|
||||
@state() private _height?: number;
|
||||
|
||||
public showDialog(params): void {
|
||||
this._params = params;
|
||||
this._group = params.group;
|
||||
if (this._params?.type === "action") {
|
||||
this.hass.loadBackendTranslation("services");
|
||||
this._fetchManifests();
|
||||
this._calculateUsedDomains();
|
||||
}
|
||||
this._fullScreen = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
if (this._params) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
this._height = undefined;
|
||||
this._width = undefined;
|
||||
this._params = undefined;
|
||||
this._group = undefined;
|
||||
this._prev = undefined;
|
||||
this._filter = "";
|
||||
this._manifests = undefined;
|
||||
this._domains = undefined;
|
||||
}
|
||||
|
||||
private _getGroups = (
|
||||
type: AddAutomationElementDialogParams["type"],
|
||||
group: string | undefined
|
||||
): AutomationElementGroup =>
|
||||
group
|
||||
? isService(group)
|
||||
? {}
|
||||
: TYPES[type].groups[group].members!
|
||||
: TYPES[type].groups;
|
||||
|
||||
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 = this._getGroups(type, group);
|
||||
|
||||
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,
|
||||
domains: Set<string> | undefined,
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
manifests?: DomainManifestLookup
|
||||
): ListItem[] => {
|
||||
if (type === "action" && isService(group)) {
|
||||
let result = this._services(localize, services, manifests, group);
|
||||
if (group === `${SERVICE_PREFIX}media_player`) {
|
||||
result = [
|
||||
this._convertToItem("play_media", {}, type, localize),
|
||||
...result,
|
||||
];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const groups = this._getGroups(type, group);
|
||||
|
||||
const result = Object.entries(groups).map(([key, options]) =>
|
||||
this._convertToItem(key, options, type, localize)
|
||||
);
|
||||
|
||||
if (type === "action") {
|
||||
if (!this._group) {
|
||||
result.unshift(
|
||||
...this._serviceGroups(
|
||||
localize,
|
||||
services,
|
||||
manifests,
|
||||
domains,
|
||||
undefined
|
||||
)
|
||||
);
|
||||
} else if (this._group === "helpers") {
|
||||
result.unshift(
|
||||
...this._serviceGroups(
|
||||
localize,
|
||||
services,
|
||||
manifests,
|
||||
domains,
|
||||
"helper"
|
||||
)
|
||||
);
|
||||
} else if (this._group === "other") {
|
||||
result.unshift(
|
||||
...this._serviceGroups(
|
||||
localize,
|
||||
services,
|
||||
manifests,
|
||||
domains,
|
||||
"other"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result.sort((a, b) => {
|
||||
if (a.group && b.group) {
|
||||
return 0;
|
||||
}
|
||||
if (a.group && !b.group) {
|
||||
return 1;
|
||||
}
|
||||
if (!a.group && b.group) {
|
||||
return -1;
|
||||
}
|
||||
return stringCompare(a.name, b.name, this.hass.locale.language);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
private _serviceGroups = (
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
manifests: DomainManifestLookup | undefined,
|
||||
domains: Set<string> | undefined,
|
||||
type: "helper" | "other" | undefined
|
||||
): ListItem[] => {
|
||||
if (!services || !manifests) {
|
||||
return [];
|
||||
}
|
||||
const result: ListItem[] = [];
|
||||
Object.keys(services).forEach((domain) => {
|
||||
const manifest = manifests[domain];
|
||||
const domainUsed = !domains ? true : domains.has(domain);
|
||||
if (
|
||||
(type === undefined &&
|
||||
(ENTITY_DOMAINS_MAIN.has(domain) ||
|
||||
(manifest?.integration_type === "entity" &&
|
||||
domainUsed &&
|
||||
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
|
||||
(type === "helper" && manifest?.integration_type === "helper") ||
|
||||
(type === "other" &&
|
||||
!ENTITY_DOMAINS_MAIN.has(domain) &&
|
||||
(ENTITY_DOMAINS_OTHER.has(domain) ||
|
||||
(!domainUsed && manifest?.integration_type === "entity") ||
|
||||
!["helper", "entity"].includes(manifest?.integration_type || "")))
|
||||
) {
|
||||
const icon = domainIconWithoutDefault(domain);
|
||||
result.push({
|
||||
group: true,
|
||||
icon,
|
||||
image: !icon
|
||||
? brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})
|
||||
: undefined,
|
||||
key: `${SERVICE_PREFIX}${domain}`,
|
||||
name: domainToName(localize, domain, manifest),
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
});
|
||||
return result.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
};
|
||||
|
||||
private _services = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
manifests: DomainManifestLookup | undefined,
|
||||
group?: string
|
||||
): ListItem[] => {
|
||||
if (!services) {
|
||||
return [];
|
||||
}
|
||||
const result: ListItem[] = [];
|
||||
|
||||
let domain: string | undefined;
|
||||
|
||||
if (isService(group)) {
|
||||
domain = getService(group!);
|
||||
}
|
||||
|
||||
const addDomain = (dmn: string) => {
|
||||
const services_keys = Object.keys(services[dmn]);
|
||||
|
||||
for (const service of services_keys) {
|
||||
const icon = domainIconWithoutDefault(dmn);
|
||||
result.push({
|
||||
group: false,
|
||||
icon,
|
||||
image: !icon
|
||||
? brandsUrl({
|
||||
domain: dmn,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})
|
||||
: undefined,
|
||||
key: `${SERVICE_PREFIX}${dmn}.${service}`,
|
||||
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
|
||||
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
|
||||
services[dmn][service]?.name ||
|
||||
service
|
||||
}`,
|
||||
description:
|
||||
this.hass.localize(
|
||||
`component.${domain}.services.${service}.description`
|
||||
) || services[dmn][service]?.description,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (domain) {
|
||||
addDomain(domain);
|
||||
return result.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
}
|
||||
|
||||
if (group && !["helpers", "other"].includes(group)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Object.keys(services)
|
||||
.sort()
|
||||
.forEach((dmn) => {
|
||||
const manifest = manifests?.[dmn];
|
||||
if (group === "helpers" && manifest?.integration_type !== "helper") {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
group === "other" &&
|
||||
(ENTITY_DOMAINS_OTHER.has(dmn) ||
|
||||
["helper", "entity"].includes(manifest?.integration_type || ""))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
addDomain(dmn);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
||||
private async _fetchManifests() {
|
||||
const manifests = {};
|
||||
const fetched = await fetchIntegrationManifests(this.hass);
|
||||
for (const manifest of fetched) {
|
||||
manifests[manifest.domain] = manifest;
|
||||
}
|
||||
this._manifests = manifests;
|
||||
}
|
||||
|
||||
private _calculateUsedDomains() {
|
||||
const domains = new Set(Object.keys(this.hass.states).map(computeDomain));
|
||||
if (!deepEqual(domains, this._domains)) {
|
||||
this._domains = domains;
|
||||
}
|
||||
}
|
||||
|
||||
protected _opened(): void {
|
||||
// Store the width and height so that when we search, box doesn't jump
|
||||
const boundingRect =
|
||||
this.shadowRoot!.querySelector("mwc-list")?.getBoundingClientRect();
|
||||
this._width = boundingRect?.width;
|
||||
this._height = boundingRect?.height;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
this._params?.type === "action" &&
|
||||
changedProperties.has("hass") &&
|
||||
changedProperties.get("hass")?.states !== this.hass.states
|
||||
) {
|
||||
this._calculateUsedDomains();
|
||||
}
|
||||
}
|
||||
|
||||
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._domains,
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
this._manifests
|
||||
);
|
||||
|
||||
const groupName = isService(this._group)
|
||||
? domainToName(
|
||||
this.hass.localize,
|
||||
getService(this._group!),
|
||||
this._manifests?.[getService(this._group!)]
|
||||
)
|
||||
: this.hass.localize(
|
||||
// @ts-ignore
|
||||
`ui.panel.config.automation.editor.${this._params.type}s.groups.${this._group}.label`
|
||||
);
|
||||
|
||||
return html`
|
||||
<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 ? `${this._width}px` : "auto",
|
||||
height: this._height ? `${Math.min(468, this._height)}px` : "auto",
|
||||
})}
|
||||
>
|
||||
${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>
|
||||
${item.icon
|
||||
? html`<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${item.icon}
|
||||
></ha-svg-icon>`
|
||||
: html`<img
|
||||
alt=""
|
||||
slot="graphic"
|
||||
src=${item.image}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>`}
|
||||
${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() {
|
||||
this._dialog!.scrollToPos(0, 0);
|
||||
if (this._filter) {
|
||||
this._filter = "";
|
||||
return;
|
||||
}
|
||||
if (this._prev) {
|
||||
this._group = this._prev;
|
||||
this._prev = undefined;
|
||||
return;
|
||||
}
|
||||
this._group = undefined;
|
||||
}
|
||||
|
||||
private _selected(ev) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
this._dialog!.scrollToPos(0, 0);
|
||||
const item = ev.currentTarget;
|
||||
if (item.group) {
|
||||
this._prev = this._group;
|
||||
this._group = item.value;
|
||||
return;
|
||||
}
|
||||
this._params!.add(item.value);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _filterChanged(ev) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
--mdc-dialog-max-height: 60vh;
|
||||
}
|
||||
@media all and (min-width: 550px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 500px;
|
||||
}
|
||||
}
|
||||
ha-icon-next {
|
||||
width: 24px;
|
||||
}
|
||||
mwc-list {
|
||||
max-height: 468px;
|
||||
max-width: 100vw;
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
margin: 0 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"add-automation-element-dialog": DialogAddAutomationElement;
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
@@ -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,
|
||||
@@ -396,13 +358,19 @@ export default class HaAutomationCondition extends LitElement {
|
||||
overflow: hidden;
|
||||
}
|
||||
.handle {
|
||||
cursor: move;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
padding: 12px;
|
||||
}
|
||||
.handle ha-svg-icon {
|
||||
pointer-events: none;
|
||||
height: 24px;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -168,6 +168,7 @@ export class HaDeviceCondition extends LitElement {
|
||||
}
|
||||
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
`;
|
||||
|
@@ -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 />`
|
||||
);
|
||||
|
@@ -328,7 +328,7 @@ class HaAutomationPicker extends LitElement {
|
||||
>
|
||||
</ha-button-related-filter-menu>
|
||||
${!this.automations.length
|
||||
? html` <div class="empty" slot="empty">
|
||||
? html`<div class="empty" slot="empty">
|
||||
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
|
||||
<h1>
|
||||
${this.hass.localize(
|
||||
@@ -338,9 +338,12 @@ class HaAutomationPicker extends LitElement {
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.empty_text_1"
|
||||
)}<br />
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.empty_text_2"
|
||||
"ui.panel.config.automation.picker.empty_text_2",
|
||||
{ user: this.hass.user?.name || "Alice" }
|
||||
)}
|
||||
</p>
|
||||
<a
|
||||
@@ -349,9 +352,7 @@ class HaAutomationPicker extends LitElement {
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.learn_more"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.common.learn_more")}
|
||||
</ha-button>
|
||||
</a>
|
||||
</div>`
|
||||
@@ -549,6 +550,7 @@ class HaAutomationPicker extends LitElement {
|
||||
.empty {
|
||||
--paper-font-headline_-_font-size: 28px;
|
||||
--mdc-icon-size: 80px;
|
||||
max-width: 500px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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,13 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
${!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 +107,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 +124,14 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
${!ensureArray(this.config.condition)?.length
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.description",
|
||||
{ user: this.hass.user?.name || "Alice" }
|
||||
)}
|
||||
</p>`
|
||||
: nothing}
|
||||
|
||||
<ha-automation-condition
|
||||
role="region"
|
||||
@@ -143,6 +163,13 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
${!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 +234,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 +246,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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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,
|
||||
});
|
||||
};
|
@@ -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>
|
||||
|
@@ -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,
|
||||
@@ -371,7 +298,8 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
overflow: hidden;
|
||||
}
|
||||
.handle {
|
||||
cursor: move;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
padding: 12px;
|
||||
}
|
||||
.handle ha-svg-icon {
|
||||
|
@@ -174,6 +174,7 @@ export class HaDeviceTrigger extends LitElement {
|
||||
}
|
||||
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
`;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -118,10 +118,12 @@ const OVERRIDE_DEVICE_CLASSES = {
|
||||
"carbon_monoxide",
|
||||
"moisture",
|
||||
], // Alarm
|
||||
["connectivity"], // Connectivity
|
||||
["update"], // Update
|
||||
],
|
||||
};
|
||||
|
||||
const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"];
|
||||
const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren", "valve"];
|
||||
|
||||
const PRECISIONS = [0, 1, 2, 3, 4, 5, 6];
|
||||
|
||||
|
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -285,7 +285,8 @@ class HaInputSelectForm extends LitElement {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.handle {
|
||||
cursor: move;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.handle ha-svg-icon {
|
||||
|
@@ -3,21 +3,22 @@ import "@material/mwc-list/mwc-list";
|
||||
import Fuse, { IFuseOptions } from "fuse.js";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import {
|
||||
protocolIntegrationPicked,
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
@@ -34,10 +35,10 @@ import {
|
||||
import {
|
||||
Brand,
|
||||
Brands,
|
||||
findIntegration,
|
||||
getIntegrationDescriptions,
|
||||
Integration,
|
||||
Integrations,
|
||||
findIntegration,
|
||||
getIntegrationDescriptions,
|
||||
} from "../../../data/integrations";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import {
|
||||
@@ -339,7 +340,9 @@ class AddIntegrationDialog extends LitElement {
|
||||
!("integrations" in integration) &&
|
||||
!this._flowsInProgress?.length
|
||||
) {
|
||||
return "What type of device is it?";
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.integrations.what_device_type"
|
||||
);
|
||||
}
|
||||
if (
|
||||
integration &&
|
||||
@@ -347,9 +350,11 @@ class AddIntegrationDialog extends LitElement {
|
||||
!("integrations" in integration) &&
|
||||
this._flowsInProgress?.length
|
||||
) {
|
||||
return "Want to add these discovered devices?";
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.integrations.confirm_add_discovered"
|
||||
);
|
||||
}
|
||||
return "What do you want to add?";
|
||||
return this.hass.localize("ui.panel.config.integrations.what_to_add");
|
||||
}
|
||||
|
||||
private _renderIntegration(
|
||||
@@ -424,8 +429,7 @@ class AddIntegrationDialog extends LitElement {
|
||||
private _renderAll(integrations?: IntegrationListItem[]): TemplateResult {
|
||||
return html`<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 +438,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"
|
||||
|
@@ -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`
|
||||
|
@@ -326,7 +326,7 @@ class DialogSystemInformation extends LitElement {
|
||||
value = html`
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
size="tiny"
|
||||
size="small"
|
||||
></ha-circular-progress>
|
||||
`;
|
||||
} else if (info.type === "failed") {
|
||||
|
@@ -249,7 +249,7 @@ class HaSceneDashboard extends LitElement {
|
||||
>
|
||||
</ha-button-related-filter-menu>
|
||||
${!this.scenes.length
|
||||
? html` <div class="empty" slot="empty">
|
||||
? html`<div class="empty" slot="empty">
|
||||
<ha-svg-icon .path=${mdiPalette}></ha-svg-icon>
|
||||
<h1>
|
||||
${this.hass.localize(
|
||||
@@ -265,9 +265,7 @@ class HaSceneDashboard extends LitElement {
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.learn_more"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.common.learn_more")}
|
||||
</ha-button>
|
||||
</a>
|
||||
</div>`
|
||||
@@ -387,6 +385,7 @@ class HaSceneDashboard extends LitElement {
|
||||
.empty {
|
||||
--paper-font-headline_-_font-size: 28px;
|
||||
--mdc-icon-size: 80px;
|
||||
max-width: 500px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -513,6 +513,7 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
if (
|
||||
!entity.device_id ||
|
||||
entity.entity_category ||
|
||||
entity.hidden_by ||
|
||||
SCENE_IGNORED_DOMAINS.includes(computeDomain(entity.entity_id))
|
||||
) {
|
||||
continue;
|
||||
|
@@ -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 />`
|
||||
);
|
||||
|
@@ -294,9 +294,7 @@ class HaScriptPicker extends LitElement {
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.picker.learn_more"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.common.learn_more")}
|
||||
</ha-button>
|
||||
</a>
|
||||
</div>`
|
||||
@@ -509,6 +507,7 @@ class HaScriptPicker extends LitElement {
|
||||
.empty {
|
||||
--paper-font-headline_-_font-size: 28px;
|
||||
--mdc-icon-size: 80px;
|
||||
max-width: 500px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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,
|
||||
|
@@ -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%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -247,7 +247,7 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
}
|
||||
|
||||
// Play audio when we're done.
|
||||
if (updatedRun.stage === "done") {
|
||||
if (updatedRun.stage === "done" && !updatedRun.error) {
|
||||
const url = updatedRun.tts!.tts_output!.url;
|
||||
const audio = new Audio(url);
|
||||
audio.addEventListener("ended", () => {
|
||||
@@ -261,7 +261,10 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
}
|
||||
});
|
||||
audio.play();
|
||||
} else if (updatedRun.stage === "error") {
|
||||
} else if (
|
||||
(updatedRun.stage === "done" && updatedRun.error) ||
|
||||
updatedRun.stage === "error"
|
||||
) {
|
||||
this._finished = true;
|
||||
}
|
||||
},
|
||||
|
@@ -90,7 +90,7 @@ const renderProgress = (
|
||||
return html`❌`;
|
||||
}
|
||||
return html`
|
||||
<ha-circular-progress size="tiny" indeterminate></ha-circular-progress>
|
||||
<ha-circular-progress size="small" indeterminate></ha-circular-progress>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,217 @@
|
||||
import { mdiFan } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-control-select";
|
||||
import type { ControlSelectOption } from "../../../components/ha-control-select";
|
||||
import "../../../components/ha-control-select-menu";
|
||||
import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu";
|
||||
import {
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
computeFanModeIcon,
|
||||
} from "../../../data/climate";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { ClimateFanModesCardFeatureConfig } from "./types";
|
||||
|
||||
export const supportsClimateFanModesCardFeature = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
domain === "climate" &&
|
||||
supportsFeature(stateObj, ClimateEntityFeature.FAN_MODE)
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-climate-fan-modes-card-feature")
|
||||
class HuiClimateFanModesCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: ClimateEntity;
|
||||
|
||||
@state() private _config?: ClimateFanModesCardFeatureConfig;
|
||||
|
||||
@state() _currentFanMode?: string;
|
||||
|
||||
@query("ha-control-select-menu", true)
|
||||
private _haSelect?: HaControlSelectMenu;
|
||||
|
||||
static getStubConfig(
|
||||
_,
|
||||
stateObj?: HassEntity
|
||||
): ClimateFanModesCardFeatureConfig {
|
||||
return {
|
||||
type: "climate-fan-modes",
|
||||
style: "dropdown",
|
||||
fan_modes: stateObj?.attributes.fan_modes || [],
|
||||
};
|
||||
}
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import(
|
||||
"../editor/config-elements/hui-climate-fan-modes-card-feature-editor"
|
||||
);
|
||||
return document.createElement("hui-climate-fan-modes-card-feature-editor");
|
||||
}
|
||||
|
||||
public setConfig(config: ClimateFanModesCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
super.willUpdate(changedProp);
|
||||
if (changedProp.has("stateObj") && this.stateObj) {
|
||||
this._currentFanMode = this.stateObj.attributes.fan_mode;
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (this._haSelect && changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (
|
||||
this.hass &&
|
||||
this.hass.formatEntityAttributeValue !==
|
||||
oldHass?.formatEntityAttributeValue
|
||||
) {
|
||||
this._haSelect.layoutOptions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _valueChanged(ev: CustomEvent) {
|
||||
const fanMode =
|
||||
(ev.detail as any).value ?? ((ev.target as any).value as string);
|
||||
|
||||
const oldFanMode = this.stateObj!.attributes.fan_mode;
|
||||
|
||||
if (fanMode === oldFanMode) return;
|
||||
|
||||
this._currentFanMode = fanMode;
|
||||
|
||||
try {
|
||||
await this._setMode(fanMode);
|
||||
} catch (err) {
|
||||
this._currentFanMode = oldFanMode;
|
||||
}
|
||||
}
|
||||
|
||||
private async _setMode(mode: string) {
|
||||
await this.hass!.callService("climate", "set_fan_mode", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
fan_mode: mode,
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
!supportsClimateFanModesCardFeature(this.stateObj)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
const modes = stateObj.attributes.fan_modes || [];
|
||||
|
||||
const options = modes
|
||||
.filter((mode) => (this._config!.fan_modes || []).includes(mode))
|
||||
.map<ControlSelectOption>((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass!.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"fan_mode",
|
||||
mode
|
||||
),
|
||||
path: computeFanModeIcon(mode),
|
||||
}));
|
||||
|
||||
if (this._config.style === "icons") {
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-control-select
|
||||
.options=${options}
|
||||
.value=${this._currentFanMode}
|
||||
@value-changed=${this._valueChanged}
|
||||
hide-label
|
||||
.ariaLabel=${this.hass!.formatEntityAttributeName(
|
||||
stateObj,
|
||||
"fan_mode"
|
||||
)}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-select>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-control-select-menu
|
||||
show-arrow
|
||||
hide-label
|
||||
.label=${this.hass!.formatEntityAttributeName(stateObj, "fan_mode")}
|
||||
.value=${this._currentFanMode}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._valueChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
|
||||
${options.map(
|
||||
(option) => html`
|
||||
<ha-list-item .value=${option.value} graphic="icon">
|
||||
<ha-svg-icon slot="graphic" .path=${option.path}></ha-svg-icon>
|
||||
${option.label}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-control-select-menu>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-control-select-menu {
|
||||
box-sizing: border-box;
|
||||
--control-select-menu-height: 40px;
|
||||
--control-select-menu-border-radius: 10px;
|
||||
line-height: 1.2;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
ha-control-select {
|
||||
--control-select-color: var(--feature-color);
|
||||
--control-select-padding: 0;
|
||||
--control-select-thickness: 40px;
|
||||
--control-select-border-radius: 10px;
|
||||
--control-select-button-border-radius: 10px;
|
||||
}
|
||||
.container {
|
||||
padding: 0 12px 12px 12px;
|
||||
width: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-climate-fan-modes-card-feature": HuiClimateFanModesCardFeature;
|
||||
}
|
||||
}
|
@@ -0,0 +1,165 @@
|
||||
import { mdiCancel, mdiCellphoneArrowDown } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import {
|
||||
UpdateEntity,
|
||||
UpdateEntityFeature,
|
||||
updateIsInstalling,
|
||||
} from "../../../data/update";
|
||||
import { showUpdateBackupDialogParams } from "../../../dialogs/update_backup/show-update-backup-dialog";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { UpdateActionsCardFeatureConfig } from "./types";
|
||||
|
||||
export const DEFAULT_UPDATE_BACKUP_OPTION = "ask";
|
||||
|
||||
export const supportsUpdateActionsCardFeature = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
domain === "update" &&
|
||||
supportsFeature(stateObj, UpdateEntityFeature.INSTALL)
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-update-actions-card-feature")
|
||||
class HuiUpdateActionsCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@state() private _config?: UpdateActionsCardFeatureConfig;
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import(
|
||||
"../editor/config-elements/hui-update-actions-card-feature-editor"
|
||||
);
|
||||
return document.createElement("hui-update-actions-card-feature-editor");
|
||||
}
|
||||
|
||||
static getStubConfig(): UpdateActionsCardFeatureConfig {
|
||||
return {
|
||||
type: "update-actions",
|
||||
backup: DEFAULT_UPDATE_BACKUP_OPTION,
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: UpdateActionsCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private get _installDisabled(): boolean {
|
||||
const stateObj = this.stateObj as UpdateEntity;
|
||||
|
||||
if (stateObj.state === UNAVAILABLE) return true;
|
||||
|
||||
const skippedVersion =
|
||||
stateObj.attributes.latest_version &&
|
||||
stateObj.attributes.skipped_version ===
|
||||
stateObj.attributes.latest_version;
|
||||
return (
|
||||
(!stateActive(stateObj) && !skippedVersion) ||
|
||||
updateIsInstalling(stateObj)
|
||||
);
|
||||
}
|
||||
|
||||
private get _skipDisabled(): boolean {
|
||||
const stateObj = this.stateObj as UpdateEntity;
|
||||
|
||||
if (stateObj.state === UNAVAILABLE) return true;
|
||||
|
||||
const skippedVersion =
|
||||
stateObj.attributes.latest_version &&
|
||||
stateObj.attributes.skipped_version ===
|
||||
stateObj.attributes.latest_version;
|
||||
return (
|
||||
skippedVersion || !stateActive(stateObj) || updateIsInstalling(stateObj)
|
||||
);
|
||||
}
|
||||
|
||||
private async _install(): Promise<void> {
|
||||
const supportsBackup = supportsFeature(
|
||||
this.stateObj!,
|
||||
UpdateEntityFeature.BACKUP
|
||||
);
|
||||
let backup = supportsBackup && this._config?.backup === "yes";
|
||||
|
||||
if (supportsBackup && this._config?.backup === "ask") {
|
||||
const response = await showUpdateBackupDialogParams(this, {});
|
||||
if (response === null) return;
|
||||
backup = response;
|
||||
}
|
||||
|
||||
this.hass!.callService("update", "install", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
backup: backup,
|
||||
});
|
||||
}
|
||||
|
||||
private async _skip(): Promise<void> {
|
||||
this.hass!.callService("update", "skip", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
!supportsUpdateActionsCardFeature(this.stateObj)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.skip"
|
||||
)}
|
||||
@click=${this._skip}
|
||||
.disabled=${this._skipDisabled}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCancel}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.update.install"
|
||||
)}
|
||||
@click=${this._install}
|
||||
.disabled=${this._installDisabled}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCellphoneArrowDown}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-control-button-group {
|
||||
margin: 0 12px 12px 12px;
|
||||
--control-button-group-spacing: 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-update-actions-card-feature": HuiUpdateActionsCardFeature;
|
||||
}
|
||||
}
|
@@ -35,6 +35,12 @@ export interface AlarmModesCardFeatureConfig {
|
||||
modes?: AlarmMode[];
|
||||
}
|
||||
|
||||
export interface ClimateFanModesCardFeatureConfig {
|
||||
type: "climate-fan-modes";
|
||||
style?: "dropdown" | "icons";
|
||||
fan_modes?: string[];
|
||||
}
|
||||
|
||||
export interface ClimateHvacModesCardFeatureConfig {
|
||||
type: "climate-hvac-modes";
|
||||
style?: "dropdown" | "icons";
|
||||
@@ -103,8 +109,14 @@ export interface LawnMowerCommandsCardFeatureConfig {
|
||||
commands?: LawnMowerCommand[];
|
||||
}
|
||||
|
||||
export interface UpdateActionsCardFeatureConfig {
|
||||
type: "update-actions";
|
||||
backup?: "yes" | "no" | "ask";
|
||||
}
|
||||
|
||||
export type LovelaceCardFeatureConfig =
|
||||
| AlarmModesCardFeatureConfig
|
||||
| ClimateFanModesCardFeatureConfig
|
||||
| ClimateHvacModesCardFeatureConfig
|
||||
| ClimatePresetModesCardFeatureConfig
|
||||
| CoverOpenCloseCardFeatureConfig
|
||||
@@ -122,7 +134,8 @@ export type LovelaceCardFeatureConfig =
|
||||
| TargetTemperatureCardFeatureConfig
|
||||
| WaterHeaterOperationModesCardFeatureConfig
|
||||
| SelectOptionsCardFeatureConfig
|
||||
| NumericInputCardFeatureConfig;
|
||||
| NumericInputCardFeatureConfig
|
||||
| UpdateActionsCardFeatureConfig;
|
||||
|
||||
export type LovelaceCardFeatureContext = {
|
||||
entity_id?: string;
|
||||
|
168
src/panels/lovelace/cards/energy/common/energy-chart-options.ts
Normal file
168
src/panels/lovelace/cards/energy/common/energy-chart-options.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { ChartOptions } from "chart.js";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import {
|
||||
addHours,
|
||||
subHours,
|
||||
differenceInDays,
|
||||
differenceInHours,
|
||||
} from "date-fns/esm";
|
||||
import { FrontendLocaleData } from "../../../../../data/translation";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
} from "../../../../../common/number/format_number";
|
||||
import { formatDateVeryShort } from "../../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../../common/datetime/format_time";
|
||||
|
||||
export function getSuggestedMax(dayDifference: number, end: Date): number {
|
||||
let suggestedMax = new Date(end);
|
||||
|
||||
// Sometimes around DST we get a time of 0:59 instead of 23:59 as expected.
|
||||
// Correct for this when showing days/months so we don't get an extra day.
|
||||
if (dayDifference > 2 && suggestedMax.getHours() === 0) {
|
||||
suggestedMax = subHours(suggestedMax, 1);
|
||||
}
|
||||
|
||||
suggestedMax.setMinutes(0, 0, 0);
|
||||
if (dayDifference > 35) {
|
||||
suggestedMax.setDate(1);
|
||||
}
|
||||
if (dayDifference > 2) {
|
||||
suggestedMax.setHours(0);
|
||||
}
|
||||
return suggestedMax.getTime();
|
||||
}
|
||||
|
||||
export function getCommonOptions(
|
||||
start: Date,
|
||||
end: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
unit?: string,
|
||||
compareStart?: Date,
|
||||
compareEnd?: Date
|
||||
): ChartOptions {
|
||||
const dayDifference = differenceInDays(end, start);
|
||||
const compare = compareStart !== undefined && compareEnd !== undefined;
|
||||
if (compare && dayDifference <= 35) {
|
||||
const difference = differenceInHours(end, start);
|
||||
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
|
||||
// If the compare period doesn't match the main period, adjust them to match
|
||||
if (differenceCompare > difference) {
|
||||
end = addHours(end, differenceCompare - difference);
|
||||
} else if (difference > differenceCompare) {
|
||||
compareEnd = addHours(compareEnd!, difference - differenceCompare);
|
||||
}
|
||||
}
|
||||
|
||||
const options: ChartOptions = {
|
||||
parsing: false,
|
||||
animation: false,
|
||||
interaction: {
|
||||
mode: "x",
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
suggestedMin: start.getTime(),
|
||||
max: getSuggestedMax(dayDifference, end),
|
||||
adapters: {
|
||||
date: {
|
||||
locale,
|
||||
config,
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
sampleSize: 5,
|
||||
autoSkipPadding: 20,
|
||||
font: (context) =>
|
||||
context.tick && context.tick.major
|
||||
? ({ weight: "bold" } as any)
|
||||
: {},
|
||||
},
|
||||
time: {
|
||||
tooltipFormat:
|
||||
dayDifference > 35
|
||||
? "monthyear"
|
||||
: dayDifference > 7
|
||||
? "date"
|
||||
: dayDifference > 2
|
||||
? "weekday"
|
||||
: dayDifference > 0
|
||||
? "datetime"
|
||||
: "hour",
|
||||
minUnit:
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
type: "linear",
|
||||
title: {
|
||||
display: true,
|
||||
text: unit,
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: (value) => formatNumber(Math.abs(value), locale),
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
position: "nearest",
|
||||
filter: (val) => val.formattedValue !== "0",
|
||||
itemSort: function (a, b) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
},
|
||||
callbacks: {
|
||||
title: (datasets) => {
|
||||
if (dayDifference > 0) {
|
||||
return datasets[0].label;
|
||||
}
|
||||
const date = new Date(datasets[0].parsed.x);
|
||||
return `${
|
||||
compare ? `${formatDateVeryShort(date, locale, config)}: ` : ""
|
||||
}${formatTime(date, locale, config)} – ${formatTime(
|
||||
addHours(date, 1),
|
||||
locale,
|
||||
config
|
||||
)}`;
|
||||
},
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
locale
|
||||
)} ${unit}`,
|
||||
},
|
||||
},
|
||||
filler: {
|
||||
propagate: false,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
bar: { borderWidth: 1.5, borderRadius: 4 },
|
||||
point: {
|
||||
hitRadius: 50,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(locale),
|
||||
};
|
||||
if (compare) {
|
||||
options.scales!.xAxisCompare = {
|
||||
...(options.scales!.x as Record<string, any>),
|
||||
suggestedMin: compareStart!.getTime(),
|
||||
max: getSuggestedMax(dayDifference, compareEnd!),
|
||||
display: false,
|
||||
};
|
||||
}
|
||||
return options;
|
||||
}
|
@@ -4,14 +4,7 @@ import {
|
||||
ChartOptions,
|
||||
ScatterDataPoint,
|
||||
} from "chart.js";
|
||||
import {
|
||||
addHours,
|
||||
differenceInDays,
|
||||
differenceInHours,
|
||||
endOfToday,
|
||||
isToday,
|
||||
startOfToday,
|
||||
} from "date-fns";
|
||||
import { endOfToday, isToday, startOfToday } from "date-fns";
|
||||
import { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -31,12 +24,7 @@ import {
|
||||
rgb2lab,
|
||||
} from "../../../../common/color/convert-color";
|
||||
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
||||
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../common/datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
} from "../../../../common/number/format_number";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import {
|
||||
@@ -56,6 +44,7 @@ import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergyGasGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { getCommonOptions } from "./common/energy-chart-options";
|
||||
|
||||
@customElement("hui-energy-gas-graph-card")
|
||||
export class HuiEnergyGasGraphCard
|
||||
@@ -159,105 +148,23 @@ export class HuiEnergyGasGraphCard
|
||||
compareStart?: Date,
|
||||
compareEnd?: Date
|
||||
): ChartOptions => {
|
||||
const dayDifference = differenceInDays(end, start);
|
||||
const compare = compareStart !== undefined && compareEnd !== undefined;
|
||||
if (compare) {
|
||||
const difference = differenceInHours(end, start);
|
||||
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
|
||||
// If the compare period doesn't match the main period, adjust them to match
|
||||
if (differenceCompare > difference) {
|
||||
end = addHours(end, differenceCompare - difference);
|
||||
} else if (difference > differenceCompare) {
|
||||
compareEnd = addHours(compareEnd!, difference - differenceCompare);
|
||||
}
|
||||
}
|
||||
|
||||
const commonOptions = getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
locale,
|
||||
config,
|
||||
unit,
|
||||
compareStart,
|
||||
compareEnd
|
||||
);
|
||||
const options: ChartOptions = {
|
||||
parsing: false,
|
||||
animation: false,
|
||||
interaction: {
|
||||
mode: "x",
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
suggestedMin: start.getTime(),
|
||||
suggestedMax: end.getTime(),
|
||||
adapters: {
|
||||
date: {
|
||||
locale,
|
||||
config,
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
sampleSize: 5,
|
||||
autoSkipPadding: 20,
|
||||
font: (context) =>
|
||||
context.tick && context.tick.major
|
||||
? ({ weight: "bold" } as any)
|
||||
: {},
|
||||
},
|
||||
time: {
|
||||
tooltipFormat:
|
||||
dayDifference > 35
|
||||
? "monthyear"
|
||||
: dayDifference > 7
|
||||
? "date"
|
||||
: dayDifference > 2
|
||||
? "weekday"
|
||||
: dayDifference > 0
|
||||
? "datetime"
|
||||
: "hour",
|
||||
minUnit:
|
||||
dayDifference > 35
|
||||
? "month"
|
||||
: dayDifference > 2
|
||||
? "day"
|
||||
: "hour",
|
||||
},
|
||||
offset: true,
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
type: "linear",
|
||||
title: {
|
||||
display: true,
|
||||
text: unit,
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
...commonOptions,
|
||||
plugins: {
|
||||
...commonOptions.plugins,
|
||||
tooltip: {
|
||||
position: "nearest",
|
||||
filter: (val) => val.formattedValue !== "0",
|
||||
itemSort: function (a, b) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
},
|
||||
...commonOptions.plugins!.tooltip,
|
||||
callbacks: {
|
||||
title: (datasets) => {
|
||||
if (dayDifference > 0) {
|
||||
return datasets[0].label;
|
||||
}
|
||||
const date = new Date(datasets[0].parsed.x);
|
||||
return `${
|
||||
compare
|
||||
? `${formatDateVeryShort(date, locale, config)}: `
|
||||
: ""
|
||||
}${formatTime(date, locale, config)} – ${formatTime(
|
||||
addHours(date, 1),
|
||||
locale,
|
||||
config
|
||||
)}`;
|
||||
},
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
locale
|
||||
)} ${unit}`,
|
||||
...commonOptions.plugins!.tooltip!.callbacks,
|
||||
footer: (contexts) => {
|
||||
if (contexts.length < 2) {
|
||||
return [];
|
||||
@@ -278,33 +185,8 @@ export class HuiEnergyGasGraphCard
|
||||
},
|
||||
},
|
||||
},
|
||||
filler: {
|
||||
propagate: false,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
bar: { borderWidth: 1.5, borderRadius: 4 },
|
||||
point: {
|
||||
hitRadius: 50,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(locale),
|
||||
};
|
||||
if (compare) {
|
||||
options.scales!.xAxisCompare = {
|
||||
...(options.scales!.x as Record<string, any>),
|
||||
suggestedMin: compareStart!.getTime(),
|
||||
suggestedMax: compareEnd!.getTime(),
|
||||
display: false,
|
||||
};
|
||||
}
|
||||
return options;
|
||||
}
|
||||
);
|
||||
|
@@ -5,9 +5,7 @@ import {
|
||||
ScatterDataPoint,
|
||||
} from "chart.js";
|
||||
import {
|
||||
addHours,
|
||||
differenceInDays,
|
||||
differenceInHours,
|
||||
endOfToday,
|
||||
isToday,
|
||||
startOfToday,
|
||||
@@ -31,12 +29,7 @@ import {
|
||||
rgb2lab,
|
||||
} from "../../../../common/color/convert-color";
|
||||
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
||||
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../common/datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
} from "../../../../common/number/format_number";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import {
|
||||
@@ -57,6 +50,7 @@ import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergySolarGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { getCommonOptions } from "./common/energy-chart-options";
|
||||
|
||||
@customElement("hui-energy-solar-graph-card")
|
||||
export class HuiEnergySolarGraphCard
|
||||
@@ -156,104 +150,23 @@ export class HuiEnergySolarGraphCard
|
||||
compareStart?: Date,
|
||||
compareEnd?: Date
|
||||
): ChartOptions => {
|
||||
const dayDifference = differenceInDays(end, start);
|
||||
const compare = compareStart !== undefined && compareEnd !== undefined;
|
||||
if (compare) {
|
||||
const difference = differenceInHours(end, start);
|
||||
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
|
||||
// If the compare period doesn't match the main period, adjust them to match
|
||||
if (differenceCompare > difference) {
|
||||
end = addHours(end, differenceCompare - difference);
|
||||
} else if (difference > differenceCompare) {
|
||||
compareEnd = addHours(compareEnd!, difference - differenceCompare);
|
||||
}
|
||||
}
|
||||
|
||||
const commonOptions = getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
locale,
|
||||
config,
|
||||
"kWh",
|
||||
compareStart,
|
||||
compareEnd
|
||||
);
|
||||
const options: ChartOptions = {
|
||||
parsing: false,
|
||||
animation: false,
|
||||
interaction: {
|
||||
mode: "x",
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
suggestedMin: start.getTime(),
|
||||
suggestedMax: end.getTime(),
|
||||
adapters: {
|
||||
date: {
|
||||
locale,
|
||||
config,
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
sampleSize: 5,
|
||||
autoSkipPadding: 20,
|
||||
font: (context) =>
|
||||
context.tick && context.tick.major
|
||||
? ({ weight: "bold" } as any)
|
||||
: {},
|
||||
},
|
||||
time: {
|
||||
tooltipFormat:
|
||||
dayDifference > 35
|
||||
? "monthyear"
|
||||
: dayDifference > 7
|
||||
? "date"
|
||||
: dayDifference > 2
|
||||
? "weekday"
|
||||
: dayDifference > 0
|
||||
? "datetime"
|
||||
: "hour",
|
||||
minUnit:
|
||||
dayDifference > 35
|
||||
? "month"
|
||||
: dayDifference > 2
|
||||
? "day"
|
||||
: "hour",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
type: "linear",
|
||||
title: {
|
||||
display: true,
|
||||
text: "kWh",
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
...commonOptions,
|
||||
plugins: {
|
||||
...commonOptions.plugins,
|
||||
tooltip: {
|
||||
position: "nearest",
|
||||
filter: (val) => val.formattedValue !== "0",
|
||||
itemSort: function (a, b) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
},
|
||||
...commonOptions.plugins!.tooltip,
|
||||
callbacks: {
|
||||
title: (datasets) => {
|
||||
if (dayDifference > 0) {
|
||||
return datasets[0].label;
|
||||
}
|
||||
const date = new Date(datasets[0].parsed.x);
|
||||
return `${
|
||||
compare
|
||||
? `${formatDateVeryShort(date, locale, config)}: `
|
||||
: ""
|
||||
}${formatTime(date, locale, config)} – ${formatTime(
|
||||
addHours(date, 1),
|
||||
locale,
|
||||
config
|
||||
)}`;
|
||||
},
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
locale
|
||||
)} kWh`,
|
||||
...commonOptions.plugins!.tooltip!.callbacks,
|
||||
footer: (contexts) => {
|
||||
const production_contexts = contexts.filter(
|
||||
(c) => c.dataset?.stack === "solar"
|
||||
@@ -277,15 +190,6 @@ export class HuiEnergySolarGraphCard
|
||||
},
|
||||
},
|
||||
},
|
||||
filler: {
|
||||
propagate: false,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
@@ -297,17 +201,7 @@ export class HuiEnergySolarGraphCard
|
||||
hitRadius: 5,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(locale),
|
||||
};
|
||||
if (compare) {
|
||||
options.scales!.xAxisCompare = {
|
||||
...(options.scales!.x as Record<string, any>),
|
||||
suggestedMin: compareStart!.getTime(),
|
||||
suggestedMax: compareEnd!.getTime(),
|
||||
display: false,
|
||||
};
|
||||
}
|
||||
return options;
|
||||
}
|
||||
);
|
||||
|
@@ -4,14 +4,7 @@ import {
|
||||
ChartOptions,
|
||||
ScatterDataPoint,
|
||||
} from "chart.js";
|
||||
import {
|
||||
addHours,
|
||||
differenceInDays,
|
||||
differenceInHours,
|
||||
endOfToday,
|
||||
isToday,
|
||||
startOfToday,
|
||||
} from "date-fns/esm";
|
||||
import { endOfToday, isToday, startOfToday } from "date-fns/esm";
|
||||
import { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -31,12 +24,7 @@ import {
|
||||
rgb2lab,
|
||||
} from "../../../../common/color/convert-color";
|
||||
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
||||
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../common/datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
} from "../../../../common/number/format_number";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
|
||||
@@ -51,6 +39,7 @@ import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergyUsageGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { getCommonOptions } from "./common/energy-chart-options";
|
||||
|
||||
interface ColorSet {
|
||||
base: string;
|
||||
@@ -155,81 +144,21 @@ export class HuiEnergyUsageGraphCard
|
||||
compareStart?: Date,
|
||||
compareEnd?: Date
|
||||
): ChartOptions => {
|
||||
const dayDifference = differenceInDays(end, start);
|
||||
const compare = compareStart !== undefined && compareEnd !== undefined;
|
||||
if (compare) {
|
||||
const difference = differenceInHours(end, start);
|
||||
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
|
||||
// If the compare period doesn't match the main period, adjust them to match
|
||||
if (differenceCompare > difference) {
|
||||
end = addHours(end, differenceCompare - difference);
|
||||
} else if (difference > differenceCompare) {
|
||||
compareEnd = addHours(compareEnd!, difference - differenceCompare);
|
||||
}
|
||||
}
|
||||
|
||||
const commonOptions = getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
locale,
|
||||
config,
|
||||
"kWh",
|
||||
compareStart,
|
||||
compareEnd
|
||||
);
|
||||
const options: ChartOptions = {
|
||||
parsing: false,
|
||||
animation: false,
|
||||
interaction: {
|
||||
mode: "x",
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
suggestedMin: start.getTime(),
|
||||
suggestedMax: end.getTime(),
|
||||
adapters: {
|
||||
date: {
|
||||
locale,
|
||||
config,
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
sampleSize: 5,
|
||||
autoSkipPadding: 20,
|
||||
font: (context) =>
|
||||
context.tick && context.tick.major
|
||||
? ({ weight: "bold" } as any)
|
||||
: {},
|
||||
},
|
||||
time: {
|
||||
tooltipFormat:
|
||||
dayDifference > 35
|
||||
? "monthyear"
|
||||
: dayDifference > 7
|
||||
? "date"
|
||||
: dayDifference > 2
|
||||
? "weekday"
|
||||
: dayDifference > 0
|
||||
? "datetime"
|
||||
: "hour",
|
||||
minUnit:
|
||||
dayDifference > 35
|
||||
? "month"
|
||||
: dayDifference > 2
|
||||
? "day"
|
||||
: "hour",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
type: "linear",
|
||||
title: {
|
||||
display: true,
|
||||
text: "kWh",
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: (value) => formatNumber(Math.abs(value), locale),
|
||||
},
|
||||
},
|
||||
},
|
||||
...commonOptions,
|
||||
plugins: {
|
||||
...commonOptions.plugins,
|
||||
tooltip: {
|
||||
position: "nearest",
|
||||
filter: (val) => val.formattedValue !== "0",
|
||||
...commonOptions.plugins!.tooltip,
|
||||
itemSort: function (a: any, b: any) {
|
||||
if (a.raw?.y > 0 && b.raw?.y < 0) {
|
||||
return -1;
|
||||
@@ -243,26 +172,7 @@ export class HuiEnergyUsageGraphCard
|
||||
return a.datasetIndex - b.datasetIndex;
|
||||
},
|
||||
callbacks: {
|
||||
title: (datasets) => {
|
||||
if (dayDifference > 0) {
|
||||
return datasets[0].label;
|
||||
}
|
||||
const date = new Date(datasets[0].parsed.x);
|
||||
return `${
|
||||
compare
|
||||
? `${formatDateVeryShort(date, locale, config)}: `
|
||||
: ""
|
||||
}${formatTime(date, locale, config)} – ${formatTime(
|
||||
addHours(date, 1),
|
||||
locale,
|
||||
config
|
||||
)}`;
|
||||
},
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
Math.abs(context.parsed.y),
|
||||
locale
|
||||
)} kWh`,
|
||||
...commonOptions.plugins!.tooltip!.callbacks,
|
||||
footer: (contexts) => {
|
||||
let totalConsumed = 0;
|
||||
let totalReturned = 0;
|
||||
@@ -292,33 +202,8 @@ export class HuiEnergyUsageGraphCard
|
||||
},
|
||||
},
|
||||
},
|
||||
filler: {
|
||||
propagate: false,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
bar: { borderWidth: 1.5, borderRadius: 4 },
|
||||
point: {
|
||||
hitRadius: 50,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(locale),
|
||||
};
|
||||
if (compare) {
|
||||
options.scales!.xAxisCompare = {
|
||||
...(options.scales!.x as Record<string, any>),
|
||||
suggestedMin: compareStart!.getTime(),
|
||||
suggestedMax: compareEnd!.getTime(),
|
||||
display: false,
|
||||
};
|
||||
}
|
||||
return options;
|
||||
}
|
||||
);
|
||||
|
@@ -4,14 +4,7 @@ import {
|
||||
ChartOptions,
|
||||
ScatterDataPoint,
|
||||
} from "chart.js";
|
||||
import {
|
||||
addHours,
|
||||
differenceInDays,
|
||||
differenceInHours,
|
||||
endOfToday,
|
||||
isToday,
|
||||
startOfToday,
|
||||
} from "date-fns";
|
||||
import { endOfToday, isToday, startOfToday } from "date-fns";
|
||||
import { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -31,12 +24,7 @@ import {
|
||||
rgb2lab,
|
||||
} from "../../../../common/color/convert-color";
|
||||
import { labBrighten, labDarken } from "../../../../common/color/lab";
|
||||
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../common/datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
} from "../../../../common/number/format_number";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import {
|
||||
@@ -56,6 +44,7 @@ import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergyWaterGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { getCommonOptions } from "./common/energy-chart-options";
|
||||
|
||||
@customElement("hui-energy-water-graph-card")
|
||||
export class HuiEnergyWaterGraphCard
|
||||
@@ -159,105 +148,23 @@ export class HuiEnergyWaterGraphCard
|
||||
compareStart?: Date,
|
||||
compareEnd?: Date
|
||||
): ChartOptions => {
|
||||
const dayDifference = differenceInDays(end, start);
|
||||
const compare = compareStart !== undefined && compareEnd !== undefined;
|
||||
if (compare) {
|
||||
const difference = differenceInHours(end, start);
|
||||
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
|
||||
// If the compare period doesn't match the main period, adjust them to match
|
||||
if (differenceCompare > difference) {
|
||||
end = addHours(end, differenceCompare - difference);
|
||||
} else if (difference > differenceCompare) {
|
||||
compareEnd = addHours(compareEnd!, difference - differenceCompare);
|
||||
}
|
||||
}
|
||||
|
||||
const commonOptions = getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
locale,
|
||||
config,
|
||||
unit,
|
||||
compareStart,
|
||||
compareEnd
|
||||
);
|
||||
const options: ChartOptions = {
|
||||
parsing: false,
|
||||
animation: false,
|
||||
interaction: {
|
||||
mode: "x",
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
suggestedMin: start.getTime(),
|
||||
suggestedMax: end.getTime(),
|
||||
adapters: {
|
||||
date: {
|
||||
locale,
|
||||
config,
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
sampleSize: 5,
|
||||
autoSkipPadding: 20,
|
||||
font: (context) =>
|
||||
context.tick && context.tick.major
|
||||
? ({ weight: "bold" } as any)
|
||||
: {},
|
||||
},
|
||||
time: {
|
||||
tooltipFormat:
|
||||
dayDifference > 35
|
||||
? "monthyear"
|
||||
: dayDifference > 7
|
||||
? "date"
|
||||
: dayDifference > 2
|
||||
? "weekday"
|
||||
: dayDifference > 0
|
||||
? "datetime"
|
||||
: "hour",
|
||||
minUnit:
|
||||
dayDifference > 35
|
||||
? "month"
|
||||
: dayDifference > 2
|
||||
? "day"
|
||||
: "hour",
|
||||
},
|
||||
offset: true,
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
type: "linear",
|
||||
title: {
|
||||
display: true,
|
||||
text: unit,
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
...commonOptions,
|
||||
plugins: {
|
||||
...commonOptions.plugins,
|
||||
tooltip: {
|
||||
position: "nearest",
|
||||
filter: (val) => val.formattedValue !== "0",
|
||||
itemSort: function (a, b) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
},
|
||||
...commonOptions.plugins!.tooltip,
|
||||
callbacks: {
|
||||
title: (datasets) => {
|
||||
if (dayDifference > 0) {
|
||||
return datasets[0].label;
|
||||
}
|
||||
const date = new Date(datasets[0].parsed.x);
|
||||
return `${
|
||||
compare
|
||||
? `${formatDateVeryShort(date, locale, config)}: `
|
||||
: ""
|
||||
}${formatTime(date, locale, config)} – ${formatTime(
|
||||
addHours(date, 1),
|
||||
locale,
|
||||
config
|
||||
)}`;
|
||||
},
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
locale
|
||||
)} ${unit}`,
|
||||
...commonOptions.plugins!.tooltip!.callbacks,
|
||||
footer: (contexts) => {
|
||||
if (contexts.length < 2) {
|
||||
return [];
|
||||
@@ -278,33 +185,8 @@ export class HuiEnergyWaterGraphCard
|
||||
},
|
||||
},
|
||||
},
|
||||
filler: {
|
||||
propagate: false,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
bar: { borderWidth: 1.5, borderRadius: 4 },
|
||||
point: {
|
||||
hitRadius: 50,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(locale),
|
||||
};
|
||||
if (compare) {
|
||||
options.scales!.xAxisCompare = {
|
||||
...(options.scales!.x as Record<string, any>),
|
||||
suggestedMin: compareStart!.getTime(),
|
||||
suggestedMax: compareEnd!.getTime(),
|
||||
display: false,
|
||||
};
|
||||
}
|
||||
return options;
|
||||
}
|
||||
);
|
||||
|
@@ -287,21 +287,40 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private handleRippleActivate(evt?: Event) {
|
||||
if (!this.hasCardAction) return;
|
||||
this._rippleHandlers.startPress(evt);
|
||||
}
|
||||
|
||||
private handleRippleDeactivate() {
|
||||
if (!this.hasCardAction) return;
|
||||
this._rippleHandlers.endPress();
|
||||
}
|
||||
|
||||
private handleRippleMouseEnter() {
|
||||
if (!this.hasCardAction) return;
|
||||
this._rippleHandlers.startHover();
|
||||
}
|
||||
|
||||
private handleRippleMouseLeave() {
|
||||
if (!this.hasCardAction) return;
|
||||
this._rippleHandlers.endHover();
|
||||
}
|
||||
|
||||
get hasCardAction() {
|
||||
return (
|
||||
!this._config?.tap_action ||
|
||||
hasAction(this._config?.tap_action) ||
|
||||
hasAction(this._config?.hold_action) ||
|
||||
hasAction(this._config?.double_tap_action)
|
||||
);
|
||||
}
|
||||
|
||||
get hasIconAction() {
|
||||
return (
|
||||
!this._config?.icon_tap_action || hasAction(this._config?.icon_tap_action)
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this.hass) {
|
||||
return nothing;
|
||||
@@ -368,8 +387,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
role=${ifDefined(this.hasCardAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this.hasCardAction ? "0" : undefined)}
|
||||
aria-labelledby="info"
|
||||
@mousedown=${this.handleRippleActivate}
|
||||
@mouseup=${this.handleRippleDeactivate}
|
||||
@@ -386,8 +405,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<div
|
||||
class="icon-container"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
role=${ifDefined(this.hasIconAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this.hasIconAction ? "0" : undefined)}
|
||||
@action=${this._handleIconAction}
|
||||
.actionHandler=${actionHandler()}
|
||||
>
|
||||
|
@@ -1,10 +1,15 @@
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import type { List } from "@material/mwc-list/mwc-list";
|
||||
import {
|
||||
mdiClock,
|
||||
mdiDelete,
|
||||
mdiDeleteSweep,
|
||||
mdiDotsVertical,
|
||||
mdiDrag,
|
||||
mdiNotificationClearAll,
|
||||
mdiPlus,
|
||||
mdiSort,
|
||||
} from "@mdi/js";
|
||||
import { endOfDay, isSameDay } from "date-fns";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@@ -22,11 +27,13 @@ import memoizeOne from "memoize-one";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-markdown-element";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
@@ -42,12 +49,15 @@ import {
|
||||
subscribeItems,
|
||||
updateItem,
|
||||
} from "../../../data/todo";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { SortableInstance } from "../../../resources/sortable";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showTodoItemEditDialog } from "../../todo/show-dialog-todo-item-editor";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { TodoListCardConfig } from "./types";
|
||||
import { sortableStyles } from "../../../resources/ha-sortable-style";
|
||||
|
||||
@customElement("hui-todo-list-card")
|
||||
export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
@@ -199,6 +209,14 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
<div class="addRow">
|
||||
${this.todoListSupportsFeature(TodoListEntityFeature.CREATE_TODO_ITEM)
|
||||
? html`
|
||||
<ha-textfield
|
||||
class="addBox"
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.add_item"
|
||||
)}
|
||||
@keydown=${this._addKeyPress}
|
||||
.disabled=${unavailable}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
class="addButton"
|
||||
.path=${mdiPlus}
|
||||
@@ -209,38 +227,55 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
@click=${this._addItem}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<ha-textfield
|
||||
class="addBox"
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.add_item"
|
||||
)}
|
||||
@keydown=${this._addKeyPress}
|
||||
.disabled=${unavailable}
|
||||
></ha-textfield>
|
||||
`
|
||||
: nothing}
|
||||
${this.todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="reorderButton"
|
||||
.path=${mdiSort}
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.reorder_items"
|
||||
)}
|
||||
@click=${this._toggleReorder}
|
||||
.disabled=${unavailable}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div id="unchecked">
|
||||
${this._renderItems(uncheckedItems, unavailable)}
|
||||
</div>
|
||||
${uncheckedItems.length
|
||||
? html`<div class="header">
|
||||
<span>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.unchecked_items"
|
||||
)}
|
||||
</span>
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.MOVE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-button-menu>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item
|
||||
@click=${this._toggleReorder}
|
||||
graphic="icon"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
this._reordering
|
||||
? "ui.panel.lovelace.cards.todo-list.exit_reorder_items"
|
||||
: "ui.panel.lovelace.cards.todo-list.reorder_items"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiSort}
|
||||
.disabled=${unavailable}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
</div>
|
||||
<mwc-list id="unchecked">
|
||||
${this._renderItems(uncheckedItems, unavailable)}
|
||||
</mwc-list>`
|
||||
: html`<p class="empty">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
|
||||
)}
|
||||
</p>`}
|
||||
${checkedItems.length
|
||||
? html`
|
||||
<div class="divider"></div>
|
||||
<div class="checked">
|
||||
<div class="header">
|
||||
<span>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.checked_items"
|
||||
@@ -249,65 +284,33 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-svg-icon
|
||||
class="clearall"
|
||||
tabindex="0"
|
||||
.path=${mdiNotificationClearAll}
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.clear_items"
|
||||
)}
|
||||
@click=${this._clearCompletedItems}
|
||||
.disabled=${unavailable}
|
||||
>
|
||||
</ha-svg-icon>`
|
||||
? html`<ha-button-menu>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item
|
||||
@click=${this._clearCompletedItems}
|
||||
graphic="icon"
|
||||
class="warning"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.clear_items"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDeleteSweep}
|
||||
.disabled=${unavailable}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
</div>
|
||||
${repeat(
|
||||
checkedItems,
|
||||
(item) => item.uid,
|
||||
(item) => html`
|
||||
<div class="editRow">
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-checkbox
|
||||
tabindex="0"
|
||||
.checked=${item.status === TodoItemStatus.Completed}
|
||||
.itemId=${item.uid}
|
||||
@change=${this._completeItem}
|
||||
.disabled=${unavailable}
|
||||
></ha-checkbox>`
|
||||
: nothing}
|
||||
<ha-textfield
|
||||
class="item"
|
||||
.disabled=${unavailable ||
|
||||
!this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)}
|
||||
.value=${item.summary}
|
||||
.itemId=${item.uid}
|
||||
@change=${this._saveEdit}
|
||||
></ha-textfield>
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
) &&
|
||||
!this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-icon-button
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.delete_item"
|
||||
)}
|
||||
class="deleteItemButton"
|
||||
.path=${mdiDelete}
|
||||
.itemId=${item.uid}
|
||||
@click=${this._deleteItem}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<mwc-list multi id="checked">
|
||||
${this._renderItems(checkedItems, unavailable)}
|
||||
</mwc-list>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
@@ -319,59 +322,93 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
${repeat(
|
||||
items,
|
||||
(item) => item.uid,
|
||||
(item) => html`
|
||||
<div class="editRow" item-id=${item.uid}>
|
||||
${this.todoListSupportsFeature(
|
||||
(item) => {
|
||||
const showDelete =
|
||||
this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
) &&
|
||||
!this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-checkbox
|
||||
tabindex="0"
|
||||
.checked=${item.status === TodoItemStatus.Completed}
|
||||
.itemId=${item.uid}
|
||||
.disabled=${unavailable}
|
||||
@change=${this._completeItem}
|
||||
></ha-checkbox>`
|
||||
: nothing}
|
||||
<ha-textfield
|
||||
class="item"
|
||||
);
|
||||
const showReorder =
|
||||
item.status !== TodoItemStatus.Completed && this._reordering;
|
||||
const due = item.due
|
||||
? item.due.includes("T")
|
||||
? new Date(item.due)
|
||||
: endOfDay(new Date(`${item.due}T00:00:00`))
|
||||
: undefined;
|
||||
const today =
|
||||
due && !item.due!.includes("T") && isSameDay(new Date(), due);
|
||||
return html`
|
||||
<ha-check-list-item
|
||||
left
|
||||
.hasMeta=${showReorder || showDelete}
|
||||
class="editRow ${classMap({
|
||||
completed: item.status === TodoItemStatus.Completed,
|
||||
multiline: Boolean(item.description || item.due),
|
||||
})}"
|
||||
.selected=${item.status === TodoItemStatus.Completed}
|
||||
.disabled=${unavailable ||
|
||||
!this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)}
|
||||
.value=${item.summary}
|
||||
item-id=${item.uid}
|
||||
.itemId=${item.uid}
|
||||
@change=${this._saveEdit}
|
||||
></ha-textfield>
|
||||
${this._reordering
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.drag_and_drop"
|
||||
)}
|
||||
class="reorderButton handle"
|
||||
.path=${mdiDrag}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
`
|
||||
: this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
) &&
|
||||
!this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-icon-button
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.delete_item"
|
||||
)}
|
||||
class="deleteItemButton"
|
||||
.path=${mdiDelete}
|
||||
.itemId=${item.uid}
|
||||
@click=${this._deleteItem}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
@change=${this._completeItem}
|
||||
@click=${this._openItem}
|
||||
@request-selected=${this._requestSelected}
|
||||
@keydown=${this._handleKeydown}
|
||||
>
|
||||
<div class="column">
|
||||
<span class="summary">${item.summary}</span>
|
||||
${item.description
|
||||
? html`<ha-markdown-element
|
||||
class="description"
|
||||
.content=${item.description}
|
||||
></ha-markdown-element>`
|
||||
: nothing}
|
||||
${due
|
||||
? html`<div class="due ${due < new Date() ? "overdue" : ""}">
|
||||
<ha-svg-icon .path=${mdiClock}></ha-svg-icon>${today
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.today"
|
||||
)
|
||||
: html`<ha-relative-time
|
||||
capitalize
|
||||
.hass=${this.hass}
|
||||
.datetime=${due}
|
||||
></ha-relative-time>`}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${showReorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.drag_and_drop"
|
||||
)}
|
||||
class="reorderButton handle"
|
||||
.path=${mdiDrag}
|
||||
slot="meta"
|
||||
>
|
||||
</ha-svg-icon>
|
||||
`
|
||||
: showDelete
|
||||
? html`<ha-icon-button
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.delete_item"
|
||||
)}
|
||||
class="deleteItemButton"
|
||||
.path=${mdiDelete}
|
||||
.itemId=${item.uid}
|
||||
slot="meta"
|
||||
@click=${this._deleteItem}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
: nothing}
|
||||
</ha-check-list-item>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
`;
|
||||
}
|
||||
@@ -401,37 +438,62 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
return this._items?.find((item) => item.uid === itemId);
|
||||
}
|
||||
|
||||
private _completeItem(ev): void {
|
||||
const item = this._getItem(ev.target.itemId);
|
||||
if (!item) {
|
||||
private _requestSelected(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _handleKeydown(ev) {
|
||||
if (ev.key === " ") {
|
||||
this._completeItem(ev);
|
||||
return;
|
||||
}
|
||||
updateItem(this.hass!, this._entityId!, {
|
||||
...item,
|
||||
status: ev.target.checked
|
||||
? TodoItemStatus.Completed
|
||||
: TodoItemStatus.NeedsAction,
|
||||
if (ev.key === "Enter") {
|
||||
this._openItem(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _openItem(ev): void {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (
|
||||
ev
|
||||
.composedPath()
|
||||
.find((el) => ["input", "a", "button"].includes(el.localName))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = this._getItem(ev.currentTarget.itemId);
|
||||
showTodoItemEditDialog(this, {
|
||||
entity: this._config!.entity!,
|
||||
item,
|
||||
});
|
||||
}
|
||||
|
||||
private _saveEdit(ev): void {
|
||||
// If name is not empty, update the item otherwise remove it
|
||||
if (ev.target.value) {
|
||||
const item = this._getItem(ev.target.itemId);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
updateItem(this.hass!, this._entityId!, {
|
||||
...item,
|
||||
summary: ev.target.value,
|
||||
});
|
||||
} else if (
|
||||
this.todoListSupportsFeature(TodoListEntityFeature.DELETE_TODO_ITEM)
|
||||
) {
|
||||
deleteItems(this.hass!, this._entityId!, [ev.target.itemId]);
|
||||
private async _completeItem(ev): Promise<void> {
|
||||
const item = this._getItem(ev.currentTarget.itemId);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.target.blur();
|
||||
await updateItem(this.hass!, this._entityId!, {
|
||||
uid: item.uid,
|
||||
summary: item.summary,
|
||||
status:
|
||||
item.status === TodoItemStatus.NeedsAction
|
||||
? TodoItemStatus.Completed
|
||||
: TodoItemStatus.NeedsAction,
|
||||
});
|
||||
await this.updateComplete;
|
||||
const newList: List = this.shadowRoot!.querySelector(
|
||||
item.status === TodoItemStatus.NeedsAction ? "#checked" : "#unchecked"
|
||||
)!;
|
||||
await newList.updateComplete;
|
||||
const items =
|
||||
item.status === TodoItemStatus.NeedsAction
|
||||
? this._getCheckedItems(this._items)
|
||||
: this._getUncheckedItems(this._items);
|
||||
const index = items.findIndex((itm) => itm.uid === item.uid);
|
||||
newList.focusItemAtIndex(index);
|
||||
}
|
||||
|
||||
private async _clearCompletedItems(): Promise<void> {
|
||||
@@ -464,7 +526,9 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
private _addItem(ev): void {
|
||||
const newItem = this._newItem;
|
||||
if (newItem.value!.length > 0) {
|
||||
createItem(this.hass!, this._entityId!, newItem.value!);
|
||||
createItem(this.hass!, this._entityId!, {
|
||||
summary: newItem.value!,
|
||||
});
|
||||
}
|
||||
|
||||
newItem.value = "";
|
||||
@@ -557,86 +621,165 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
return [
|
||||
sortableStyles,
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.has-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
.has-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.editRow,
|
||||
.addRow,
|
||||
.checked {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.addRow {
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.addRow ha-icon-button {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 16px;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
margin-left: -12px;
|
||||
margin-inline-start: -12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.addRow,
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.deleteItemButton {
|
||||
margin-right: -12px;
|
||||
margin-inline-end: -12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.header {
|
||||
padding-left: 30px;
|
||||
padding-right: 16px;
|
||||
padding-inline-start: 30px;
|
||||
padding-inline-end: 16px;
|
||||
margin-top: 8px;
|
||||
justify-content: space-between;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.reorderButton {
|
||||
margin-right: -12px;
|
||||
margin-inline-end: -12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.header span {
|
||||
color: var(--primary-text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: move;
|
||||
}
|
||||
.empty {
|
||||
padding: 16px 32px;
|
||||
}
|
||||
|
||||
ha-checkbox {
|
||||
margin-left: -12px;
|
||||
margin-inline-start: -12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.item {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
flex-grow: 1;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-meta-size: 56px;
|
||||
min-height: 56px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.checked {
|
||||
margin: 12px 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-check-list-item.multiline {
|
||||
align-items: flex-start;
|
||||
--check-list-item-graphic-margin-top: 8px;
|
||||
}
|
||||
|
||||
.checked span {
|
||||
color: var(--primary-text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
margin: 10px 0;
|
||||
}
|
||||
.multiline .column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.clearall {
|
||||
cursor: pointer;
|
||||
}
|
||||
.completed .summary {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todoList {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
}
|
||||
`;
|
||||
.description,
|
||||
.due {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.description {
|
||||
white-space: initial;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.description p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.due {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.due ha-svg-icon {
|
||||
margin-right: 4px;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
|
||||
.due.overdue {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.completed .due.overdue {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
height: 24px;
|
||||
padding: 16px 4px;
|
||||
}
|
||||
|
||||
.deleteItemButton {
|
||||
position: relative;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.clearall {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.todoList {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user