mirror of
https://github.com/home-assistant/frontend.git
synced 2025-10-07 18:59:50 +00:00
Compare commits
63 Commits
theme-by-u
...
improve_ne
Author | SHA1 | Date | |
---|---|---|---|
![]() |
da58dfe133 | ||
![]() |
2801d071ba | ||
![]() |
71b65f208f | ||
![]() |
ab4efb7412 | ||
![]() |
c7a46ec25b | ||
![]() |
83d4a408f6 | ||
![]() |
06932d1479 | ||
![]() |
24211d5f25 | ||
![]() |
d387f19a31 | ||
![]() |
347ee2a4c3 | ||
![]() |
1363884773 | ||
![]() |
0256da511d | ||
![]() |
c52217c1ce | ||
![]() |
cdd17eed2e | ||
![]() |
4546c6f624 | ||
![]() |
2c34760204 | ||
![]() |
0b64861297 | ||
![]() |
94a5e737cc | ||
![]() |
05163588fc | ||
![]() |
ee64536862 | ||
![]() |
695a6a506e | ||
![]() |
3ee3cfa6cb | ||
![]() |
00d0cb7afa | ||
![]() |
3ae34403bd | ||
![]() |
1434966170 | ||
![]() |
8dd70f7017 | ||
![]() |
84a0289e1b | ||
![]() |
a25e1d3f7f | ||
![]() |
f53ac41eee | ||
![]() |
b9acd40b0f | ||
![]() |
7524dc8709 | ||
![]() |
cbedf62c39 | ||
![]() |
63a98155cd | ||
![]() |
7369b7e0d5 | ||
![]() |
922abafabf | ||
![]() |
f1bb4a5694 | ||
![]() |
e0b9cb8ccb | ||
![]() |
06f27650da | ||
![]() |
a772eaffd7 | ||
![]() |
c39be4a9b8 | ||
![]() |
0abccb88d6 | ||
![]() |
5dc5879773 | ||
![]() |
41df7a3f4a | ||
![]() |
920ec035c5 | ||
![]() |
043e8d6e2e | ||
![]() |
d8e36894a0 | ||
![]() |
65b6a3c6a3 | ||
![]() |
b16f82cedb | ||
![]() |
02deeb4ce7 | ||
![]() |
0c6651c2c2 | ||
![]() |
abbf56db1d | ||
![]() |
bc0cc8b387 | ||
![]() |
b66f41db7d | ||
![]() |
05fbe204c5 | ||
![]() |
ee199fbbc0 | ||
![]() |
56ab29da81 | ||
![]() |
10abaa538d | ||
![]() |
f25dac7f68 | ||
![]() |
99065a689f | ||
![]() |
ac88d5993a | ||
![]() |
b09ce45d31 | ||
![]() |
78e2809fe7 | ||
![]() |
a631bf9854 |
@@ -5,7 +5,7 @@
|
||||
"context": ".."
|
||||
},
|
||||
"appPort": "8124:8123",
|
||||
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
|
||||
"postCreateCommand": "./.devcontainer/post_create.sh",
|
||||
"postStartCommand": "script/bootstrap",
|
||||
"containerEnv": {
|
||||
"DEV_CONTAINER": "1",
|
||||
|
22
.devcontainer/post_create.sh
Executable file
22
.devcontainer/post_create.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script will run after the container is created
|
||||
|
||||
# add github cli
|
||||
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
|
||||
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
||||
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
||||
|
||||
# Update package lists
|
||||
sudo apt-get update
|
||||
|
||||
sudo apt upgrade -y
|
||||
|
||||
# Install necessary packages
|
||||
sudo apt-get install -y libpcap-dev gh
|
||||
|
||||
# Display a message
|
||||
echo "Post-create script has been executed successfully."
|
42
.vscode/tasks.json
vendored
42
.vscode/tasks.json
vendored
@@ -1,6 +1,42 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Develop and serve Frontend",
|
||||
"type": "shell",
|
||||
"command": "script/develop_and_serve -c ${input:coreUrl}",
|
||||
// Sync changes here to other tasks until issue resolved
|
||||
// https://github.com/Microsoft/vscode/issues/61497
|
||||
"problemMatcher": {
|
||||
"owner": "ha-build",
|
||||
"source": "ha-build",
|
||||
"fileLocation": "absolute",
|
||||
"severity": "error",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
|
||||
"severity": 1,
|
||||
"file": 2,
|
||||
"message": 3,
|
||||
"line": 4,
|
||||
"column": 5
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "Changes detected. Starting compilation",
|
||||
"endsPattern": "Build done @"
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Develop Frontend",
|
||||
"type": "gulp",
|
||||
@@ -241,6 +277,12 @@
|
||||
"id": "supervisorToken",
|
||||
"type": "promptString",
|
||||
"description": "The token for the Remote API proxy add-on"
|
||||
},
|
||||
{
|
||||
"id": "coreUrl",
|
||||
"type": "promptString",
|
||||
"description": "The URL of the Home Assistant Core instance",
|
||||
"default": "http://127.0.0.1:8123"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
// @ts-check
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
},
|
||||
export default tseslint.config(...rootConfig, {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
@@ -90,6 +90,14 @@ function copyMapPanel(staticDir) {
|
||||
npmPath("leaflet/dist/leaflet.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("leaflet.markercluster/dist/MarkerCluster.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("leaflet.markercluster/dist/MarkerCluster.Default.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
fs.copySync(
|
||||
npmPath("leaflet/dist/images"),
|
||||
staticPath("images/leaflet/images/")
|
||||
|
@@ -1,11 +1,16 @@
|
||||
// @ts-check
|
||||
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import tseslint from "typescript-eslint";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import { configs as litConfigs } from "eslint-plugin-lit";
|
||||
import { configs as wcConfigs } from "eslint-plugin-wc";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
@@ -15,17 +20,14 @@ const compat = new FlatCompat({
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
...compat.extends(
|
||||
"airbnb-base",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/strict",
|
||||
"plugin:@typescript-eslint/stylistic",
|
||||
"plugin:wc/recommended",
|
||||
"plugin:lit/all",
|
||||
"plugin:lit-a11y/recommended",
|
||||
"prettier"
|
||||
),
|
||||
export default tseslint.config(
|
||||
...compat.extends("airbnb-base", "plugin:lit-a11y/recommended"),
|
||||
eslintConfigPrettier,
|
||||
litConfigs["flat/all"],
|
||||
tseslint.configs.recommended,
|
||||
tseslint.configs.strict,
|
||||
tseslint.configs.stylistic,
|
||||
wcConfigs["flat/recommended"],
|
||||
{
|
||||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
@@ -43,7 +45,7 @@ export default [
|
||||
Polymer: true,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
parser: tseslint.parser,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
|
||||
@@ -184,5 +186,5 @@ export default [
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
@@ -1,10 +1,10 @@
|
||||
// @ts-check
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
},
|
||||
export default tseslint.config(...rootConfig, {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
71
package.json
71
package.json
@@ -26,25 +26,25 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.7",
|
||||
"@babel/runtime": "7.26.9",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.4",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/commands": "6.8.0",
|
||||
"@codemirror/language": "6.10.8",
|
||||
"@codemirror/legacy-modes": "6.4.2",
|
||||
"@codemirror/search": "6.5.8",
|
||||
"@codemirror/legacy-modes": "6.4.3",
|
||||
"@codemirror/search": "6.5.9",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.36.2",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.17.2",
|
||||
"@formatjs/intl-displaynames": "6.8.9",
|
||||
"@formatjs/intl-durationformat": "0.7.2",
|
||||
"@formatjs/intl-datetimeformat": "6.17.3",
|
||||
"@formatjs/intl-displaynames": "6.8.10",
|
||||
"@formatjs/intl-durationformat": "0.7.3",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.4",
|
||||
"@formatjs/intl-listformat": "7.7.9",
|
||||
"@formatjs/intl-locale": "4.2.9",
|
||||
"@formatjs/intl-numberformat": "8.15.2",
|
||||
"@formatjs/intl-pluralrules": "5.4.2",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.9",
|
||||
"@formatjs/intl-listformat": "7.7.10",
|
||||
"@formatjs/intl-locale": "4.2.10",
|
||||
"@formatjs/intl-numberformat": "8.15.3",
|
||||
"@formatjs/intl-pluralrules": "5.4.3",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.10",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
@@ -53,9 +53,9 @@
|
||||
"@fullcalendar/timegrid": "6.1.15",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.7",
|
||||
"@lit-labs/observers": "2.0.4",
|
||||
"@lit-labs/virtualizer": "2.0.15",
|
||||
"@lit-labs/motion": "1.0.8",
|
||||
"@lit-labs/observers": "2.0.5",
|
||||
"@lit-labs/virtualizer": "2.1.0",
|
||||
"@lrnwebcomponents/simple-tooltip": "8.0.2",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
@@ -91,8 +91,8 @@
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.6.4",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.4",
|
||||
"@vaadin/combo-box": "24.6.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.5",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
@@ -116,16 +116,18 @@
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.7.14",
|
||||
"intl-messageformat": "10.7.15",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "2.8.0",
|
||||
"lit-html": "2.8.0",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "15.0.6",
|
||||
"marked": "15.0.7",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
"qrcode": "1.5.4",
|
||||
@@ -137,7 +139,7 @@
|
||||
"tinykeys": "3.0.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "2.0.1",
|
||||
"ua-parser-js": "2.0.2",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-network": "9.1.9",
|
||||
"vue": "2.7.16",
|
||||
@@ -152,20 +154,20 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.7",
|
||||
"@babel/core": "7.26.9",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.3",
|
||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
||||
"@babel/plugin-transform-runtime": "7.25.9",
|
||||
"@babel/preset-env": "7.26.7",
|
||||
"@babel/plugin-transform-runtime": "7.26.9",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.18.2",
|
||||
"@lokalise/node-api": "13.0.0",
|
||||
"@octokit/auth-oauth-device": "7.1.2",
|
||||
"@octokit/plugin-retry": "7.1.3",
|
||||
"@octokit/rest": "21.1.0",
|
||||
"@lokalise/node-api": "13.1.0",
|
||||
"@octokit/auth-oauth-device": "7.1.3",
|
||||
"@octokit/plugin-retry": "7.1.4",
|
||||
"@octokit/rest": "21.1.1",
|
||||
"@rsdoctor/rspack-plugin": "0.4.13",
|
||||
"@rspack/cli": "1.2.2",
|
||||
"@rspack/core": "1.2.2",
|
||||
"@rspack/cli": "1.2.3",
|
||||
"@rspack/core": "1.2.3",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.21",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -175,6 +177,7 @@
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.16",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/leaflet.markercluster": "1.5.5",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/mocha": "10.0.10",
|
||||
@@ -183,14 +186,12 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "8.23.0",
|
||||
"@typescript-eslint/parser": "8.23.0",
|
||||
"@vitest/coverage-v8": "3.0.5",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.20.0",
|
||||
"eslint": "9.20.1",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -215,9 +216,8 @@
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"object-hash": "3.0.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.4.2",
|
||||
"prettier": "3.5.1",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"serve": "14.2.4",
|
||||
"sinon": "19.0.2",
|
||||
@@ -225,6 +225,7 @@
|
||||
"terser-webpack-plugin": "5.3.11",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.24.1",
|
||||
"vitest": "3.0.5",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
@@ -239,7 +240,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"globals": "15.14.0",
|
||||
"globals": "15.15.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0"
|
||||
|
@@ -136,11 +136,18 @@ export function theme2hex(themeColor: string): string {
|
||||
}
|
||||
|
||||
const rgbFromColorName = colors[themeColor];
|
||||
if (!rgbFromColorName) {
|
||||
// We have a named color, and there's nothing in the table,
|
||||
// so nothing further we can do with it.
|
||||
// Compare/border/background color will all be the same.
|
||||
return themeColor;
|
||||
if (rgbFromColorName) {
|
||||
return rgb2hex(rgbFromColorName);
|
||||
}
|
||||
return rgb2hex(rgbFromColorName);
|
||||
|
||||
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch.map(Number);
|
||||
return rgb2hex([r, g, b]);
|
||||
}
|
||||
|
||||
// We have a named color, and there's nothing in the table,
|
||||
// so nothing further we can do with it.
|
||||
// Compare/border/background color will all be the same.
|
||||
return themeColor;
|
||||
}
|
||||
|
@@ -16,11 +16,30 @@ export const setupLeafletMap = async (
|
||||
const Leaflet = (await import("leaflet")).default as LeafletModuleType;
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||
|
||||
await import("leaflet.markercluster");
|
||||
|
||||
const map = Leaflet.map(mapElement);
|
||||
const style = document.createElement("link");
|
||||
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
||||
style.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(style);
|
||||
|
||||
const markerClusterStyle = document.createElement("link");
|
||||
markerClusterStyle.setAttribute(
|
||||
"href",
|
||||
"/static/images/leaflet/MarkerCluster.css"
|
||||
);
|
||||
markerClusterStyle.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(markerClusterStyle);
|
||||
|
||||
const defaultMarkerClusterStyle = document.createElement("link");
|
||||
defaultMarkerClusterStyle.setAttribute(
|
||||
"href",
|
||||
"/static/images/leaflet/MarkerCluster.Default.css"
|
||||
);
|
||||
defaultMarkerClusterStyle.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(defaultMarkerClusterStyle);
|
||||
|
||||
map.setView([52.3731339, 4.8903147], 13);
|
||||
|
||||
const tileLayer = createTileLayer(Leaflet).addTo(map);
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { stringCompare } from "../string/compare";
|
||||
|
||||
export const FIXED_DOMAIN_STATES = {
|
||||
alarm_control_panel: [
|
||||
@@ -237,6 +240,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
};
|
||||
|
||||
export const getStates = (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
attribute: string | undefined = undefined
|
||||
): string[] => {
|
||||
@@ -269,7 +273,19 @@ export const getStates = (
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
if (!attribute) {
|
||||
result.push("home", "not_home");
|
||||
result.push(
|
||||
...Object.entries(hass.states)
|
||||
.filter(
|
||||
([entityId, stateObj]) =>
|
||||
computeDomain(entityId) === "zone" &&
|
||||
entityId !== "zone.home" &&
|
||||
stateObj.attributes.friendly_name
|
||||
)
|
||||
.map(([_entityId, stateObj]) => stateObj.attributes.friendly_name!)
|
||||
.sort((zone1, zone2) =>
|
||||
stringCompare(zone1, zone2, hass.locale.language)
|
||||
)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
|
32
src/common/map/decorated_marker.ts
Normal file
32
src/common/map/decorated_marker.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { LatLngExpression, Layer, Map, MarkerOptions } from "leaflet";
|
||||
import { Marker } from "leaflet";
|
||||
|
||||
export class DecoratedMarker extends Marker {
|
||||
decorationLayer: Layer | undefined;
|
||||
|
||||
constructor(
|
||||
latlng: LatLngExpression,
|
||||
decorationLayer?: Layer,
|
||||
options?: MarkerOptions
|
||||
) {
|
||||
super(latlng, options);
|
||||
|
||||
this.decorationLayer = decorationLayer;
|
||||
}
|
||||
|
||||
onAdd(map: Map) {
|
||||
super.onAdd(map);
|
||||
|
||||
// If decoration has been provided, add it to the map as well
|
||||
this.decorationLayer?.addTo(map);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
onRemove(map: Map) {
|
||||
// If decoration has been provided, remove it from the map as well
|
||||
this.decorationLayer?.remove();
|
||||
|
||||
return super.onRemove(map);
|
||||
}
|
||||
}
|
@@ -24,6 +24,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../ha-icon-button";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
|
||||
@@ -67,12 +68,16 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _listeners: (() => void)[] = [];
|
||||
|
||||
private _originalZrFlush?: () => void;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
}
|
||||
this.chart?.dispose();
|
||||
this.chart = undefined;
|
||||
this._originalZrFlush = undefined;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -83,19 +88,19 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
this._listeners.push(
|
||||
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
||||
this._reducedMotion = matches;
|
||||
this.chart?.setOption({ animation: !this._reducedMotion });
|
||||
if (this._reducedMotion !== matches) {
|
||||
this._reducedMotion = matches;
|
||||
this._setChartOptions({ animation: !this._reducedMotion });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Add keyboard event listeners
|
||||
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||
if ((isMac && ev.metaKey) || (!isMac && ev.ctrlKey)) {
|
||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||
this._modifierPressed = true;
|
||||
if (!this.options?.dataZoom) {
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -104,9 +109,7 @@ export class HaChartBase extends LitElement {
|
||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||
this._modifierPressed = false;
|
||||
if (!this.options?.dataZoom) {
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -124,27 +127,24 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated || !this.chart) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("_themes")) {
|
||||
this._setupChart();
|
||||
return;
|
||||
}
|
||||
let chartOptions: ECOption = {};
|
||||
if (changedProps.has("data")) {
|
||||
this.chart.setOption(
|
||||
{ series: this.data },
|
||||
{ lazyUpdate: true, replaceMerge: ["series"] }
|
||||
);
|
||||
chartOptions.series = this.data;
|
||||
}
|
||||
if (changedProps.has("options") || changedProps.has("_isZoomed")) {
|
||||
this.chart.setOption(this._createOptions(), {
|
||||
lazyUpdate: true,
|
||||
// if we replace the whole object, it will reset the dataZoom
|
||||
replaceMerge: ["grid"],
|
||||
});
|
||||
if (changedProps.has("options")) {
|
||||
chartOptions = { ...chartOptions, ...this._createOptions() };
|
||||
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
|
||||
chartOptions.dataZoom = this._getDataZoomConfig();
|
||||
}
|
||||
if (Object.keys(chartOptions).length > 0) {
|
||||
this._setChartOptions(chartOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,6 @@ export class HaChartBase extends LitElement {
|
||||
style=${styleMap({
|
||||
height: this.height ?? `${this._getDefaultHeight()}px`,
|
||||
})}
|
||||
@wheel=${this._handleWheel}
|
||||
>
|
||||
<div class="chart"></div>
|
||||
${this._isZoomed
|
||||
@@ -240,8 +239,8 @@ export class HaChartBase extends LitElement {
|
||||
type: "inside",
|
||||
orient: "horizontal",
|
||||
filterMode: "none",
|
||||
moveOnMouseMove: this._isZoomed,
|
||||
preventDefaultMouseMove: this._isZoomed,
|
||||
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
zoomLock: !this._isTouchDevice && !this._modifierPressed,
|
||||
};
|
||||
}
|
||||
@@ -512,25 +511,33 @@ export class HaChartBase extends LitElement {
|
||||
return Math.max(this.clientWidth / 2, 200);
|
||||
}
|
||||
|
||||
private _handleZoomReset() {
|
||||
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
||||
this._modifierPressed = false;
|
||||
}
|
||||
|
||||
private _handleWheel(e: WheelEvent) {
|
||||
// if the window is not focused, we don't receive the keydown events but scroll still works
|
||||
if (!this.options?.dataZoom) {
|
||||
const modifierPressed = (isMac && e.metaKey) || (!isMac && e.ctrlKey);
|
||||
if (modifierPressed) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (modifierPressed !== this._modifierPressed) {
|
||||
this._modifierPressed = modifierPressed;
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
private _setChartOptions(options: ECOption) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (!this._originalZrFlush) {
|
||||
const dataSize = ensureArray(this.data).reduce(
|
||||
(acc, series) => acc + (series.data as any[]).length,
|
||||
0
|
||||
);
|
||||
if (dataSize > 10000) {
|
||||
// for large datasets zr.flush takes 30-40% of the render time
|
||||
// so we delay it a bit to avoid blocking the main thread
|
||||
const zr = this.chart.getZr();
|
||||
this._originalZrFlush = zr.flush.bind(zr);
|
||||
zr.flush = () => {
|
||||
setTimeout(() => {
|
||||
this._originalZrFlush?.();
|
||||
}, 10);
|
||||
};
|
||||
}
|
||||
}
|
||||
const replaceMerge = options.series ? ["series"] : [];
|
||||
this.chart.setOption(options, { replaceMerge });
|
||||
}
|
||||
|
||||
private _handleZoomReset() {
|
||||
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
@@ -75,6 +75,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@state() private _yWidth = 25;
|
||||
|
||||
@state() private _visualMap?: VisualMapComponentOption[];
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
protected render() {
|
||||
@@ -92,7 +94,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTooltip(params: any) {
|
||||
private _renderTooltip = (params: any) => {
|
||||
const time = params[0].axisValue;
|
||||
const title =
|
||||
formatDateTimeWithSeconds(
|
||||
@@ -115,7 +117,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
return;
|
||||
}
|
||||
// If the datapoint is not found, we need to find the last datapoint before the current time
|
||||
let lastData;
|
||||
let lastData: any;
|
||||
const data = dataset.data || [];
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const point = data[i];
|
||||
@@ -175,7 +177,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
})
|
||||
.join("<br>")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
this._hiddenStats.add(ev.detail.name);
|
||||
@@ -208,8 +210,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
changedProps.has("minYAxis") ||
|
||||
changedProps.has("maxYAxis") ||
|
||||
changedProps.has("fitYData") ||
|
||||
changedProps.has("_chartData") ||
|
||||
changedProps.has("paddingYAxis") ||
|
||||
changedProps.has("_visualMap") ||
|
||||
changedProps.has("_yWidth")
|
||||
) {
|
||||
const rtl = computeRTL(this.hass);
|
||||
@@ -280,37 +282,11 @@ export class StateHistoryChartLine extends LitElement {
|
||||
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
|
||||
bottom: 30,
|
||||
},
|
||||
visualMap: this._chartData
|
||||
.map((_, seriesIndex) => {
|
||||
const dataIndex = this._datasetToDataIndex[seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (!data.statistics || data.statistics.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// render stat data with a slightly transparent line
|
||||
const firstStateTS =
|
||||
data.states[0]?.last_changed ?? this.endTime.getTime();
|
||||
return {
|
||||
show: false,
|
||||
seriesIndex,
|
||||
dimension: 0,
|
||||
pieces: [
|
||||
{
|
||||
max: firstStateTS - 0.01,
|
||||
colorAlpha: 0.5,
|
||||
},
|
||||
{
|
||||
min: firstStateTS,
|
||||
colorAlpha: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as VisualMapComponentOption[],
|
||||
visualMap: this._visualMap,
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
appendTo: document.body,
|
||||
formatter: this._renderTooltip.bind(this),
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -725,6 +701,33 @@ export class StateHistoryChartLine extends LitElement {
|
||||
this._chartData = datasets;
|
||||
this._entityIds = entityIds;
|
||||
this._datasetToDataIndex = datasetToDataIndex;
|
||||
const visualMap: VisualMapComponentOption[] = [];
|
||||
this._chartData.forEach((_, seriesIndex) => {
|
||||
const dataIndex = this._datasetToDataIndex[seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (!data.statistics || data.statistics.length === 0) {
|
||||
return;
|
||||
}
|
||||
// render stat data with a slightly transparent line
|
||||
const firstStateTS =
|
||||
data.states[0]?.last_changed ?? this.endTime.getTime();
|
||||
visualMap.push({
|
||||
show: false,
|
||||
seriesIndex,
|
||||
dimension: 0,
|
||||
pieces: [
|
||||
{
|
||||
max: firstStateTS - 0.01,
|
||||
colorAlpha: 0.5,
|
||||
},
|
||||
{
|
||||
min: firstStateTS,
|
||||
colorAlpha: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
|
||||
}
|
||||
|
||||
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||
|
@@ -273,11 +273,13 @@ export class StatisticsChart extends LitElement {
|
||||
this._chartOptions = {
|
||||
xAxis: [
|
||||
{
|
||||
id: "xAxis",
|
||||
type: "time",
|
||||
min: startTime,
|
||||
max: endTime,
|
||||
max: this.endTime,
|
||||
},
|
||||
{
|
||||
id: "hiddenAxis",
|
||||
type: "time",
|
||||
show: false,
|
||||
},
|
||||
@@ -368,7 +370,6 @@ export class StatisticsChart extends LitElement {
|
||||
if (endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
this.endTime = endTime;
|
||||
|
||||
let unit: string | undefined | null;
|
||||
|
||||
|
@@ -448,6 +448,7 @@ export class HaDataTable extends LitElement {
|
||||
)}
|
||||
@click=${this._handleHeaderClick}
|
||||
.columnId=${key}
|
||||
title=${ifDefined(column.title)}
|
||||
>
|
||||
${column.sortable
|
||||
? html`
|
||||
|
@@ -57,7 +57,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
(this._comboBox as any).items = [
|
||||
...(this.extraOptions ?? []),
|
||||
...(this.entityId && stateObj
|
||||
? getStates(stateObj, this.attribute).map((key) => ({
|
||||
? getStates(this.hass, stateObj, this.attribute).map((key) => ({
|
||||
value: key,
|
||||
label: !this.attribute
|
||||
? this.hass.formatEntityState(stateObj, key)
|
||||
|
@@ -5,15 +5,16 @@ import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import {
|
||||
addDays,
|
||||
subHours,
|
||||
endOfDay,
|
||||
endOfMonth,
|
||||
endOfWeek,
|
||||
endOfYear,
|
||||
isThisYear,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
startOfYear,
|
||||
isThisYear,
|
||||
} from "date-fns";
|
||||
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
@@ -178,6 +179,96 @@ export class HaDateRangePicker extends LitElement {
|
||||
weekStartsOn,
|
||||
}),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-1h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
1
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-12h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
12
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-24h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-7d"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24 * 7
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-30d"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24 * 30
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
@@ -395,44 +486,55 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-icon-button {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
direction: var(--direction);
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.date-range-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-textarea {
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
}
|
||||
@media only screen and (max-width: 460px) {
|
||||
ha-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 940px) and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
overflow: auto;
|
||||
max-height: calc(70vh - 330px);
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.date-range-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
:host([header-position]) .date-range-ranges {
|
||||
max-height: calc(90vh - 430px);
|
||||
}
|
||||
|
||||
ha-textarea {
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
}
|
||||
@media only screen and (max-width: 460px) {
|
||||
ha-textarea {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,7 +1,12 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import hash from "object-hash";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { renderMarkdown } from "../resources/render-markdown";
|
||||
import { CacheManager } from "../util/cache-manager";
|
||||
|
||||
const markdownCache = new CacheManager<string>(1000);
|
||||
|
||||
const _gitHubMarkdownAlerts = {
|
||||
reType:
|
||||
@@ -26,6 +31,16 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
markdownCache.set(key, this.innerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
@@ -37,6 +52,24 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (!this.innerHTML && this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
if (markdownCache.has(key)) {
|
||||
this.innerHTML = markdownCache.get(key)!;
|
||||
this._resize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _computeCacheKey() {
|
||||
return hash({
|
||||
content: this.content,
|
||||
allowSvg: this.allowSvg,
|
||||
breaks: this.breaks,
|
||||
});
|
||||
}
|
||||
|
||||
private async _render() {
|
||||
this.innerHTML = await renderMarkdown(
|
||||
String(this.content),
|
||||
|
@@ -13,6 +13,8 @@ export class HaMarkdown extends LitElement {
|
||||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.content) {
|
||||
return nothing;
|
||||
@@ -23,6 +25,7 @@ export class HaMarkdown extends LitElement {
|
||||
.allowSvg=${this.allowSvg}
|
||||
.breaks=${this.breaks}
|
||||
.lazyImages=${this.lazyImages}
|
||||
.cache=${this.cache}
|
||||
></ha-markdown-element>`;
|
||||
}
|
||||
|
||||
|
@@ -8,9 +8,10 @@ import type {
|
||||
Map,
|
||||
Marker,
|
||||
Polyline,
|
||||
MarkerClusterGroup,
|
||||
} from "leaflet";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement, css } from "lit";
|
||||
import { css, ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
@@ -26,6 +27,7 @@ import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
import "../ha-icon-button";
|
||||
import "./ha-entity-marker";
|
||||
import { DecoratedMarker } from "../../common/map/decorated_marker";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -84,6 +86,9 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
@property({ type: Number }) public zoom = 14;
|
||||
|
||||
@property({ attribute: "cluster-markers", type: Boolean })
|
||||
public clusterMarkers = true;
|
||||
|
||||
@state() private _loaded = false;
|
||||
|
||||
public leafletMap?: Map;
|
||||
@@ -96,10 +101,12 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private _mapFocusItems: (Marker | Circle)[] = [];
|
||||
|
||||
private _mapZones: (Marker | Circle)[] = [];
|
||||
private _mapZones: DecoratedMarker[] = [];
|
||||
|
||||
private _mapFocusZones: (Marker | Circle)[] = [];
|
||||
|
||||
private _mapCluster: MarkerClusterGroup | undefined;
|
||||
|
||||
private _mapPaths: (Polyline | CircleMarker)[] = [];
|
||||
|
||||
private _clickCount = 0;
|
||||
@@ -151,6 +158,10 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("clusterMarkers")) {
|
||||
this._drawEntities();
|
||||
}
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("paths")) {
|
||||
this._drawPaths();
|
||||
}
|
||||
@@ -175,6 +186,7 @@ export class HaMap extends ReactiveElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateMapStyle();
|
||||
}
|
||||
|
||||
@@ -426,6 +438,11 @@ export class HaMap extends ReactiveElement {
|
||||
this._mapFocusZones = [];
|
||||
}
|
||||
|
||||
if (this._mapCluster) {
|
||||
this._mapCluster.remove();
|
||||
this._mapCluster = undefined;
|
||||
}
|
||||
|
||||
if (!this.entities) {
|
||||
return;
|
||||
}
|
||||
@@ -481,26 +498,24 @@ export class HaMap extends ReactiveElement {
|
||||
iconHTML = el.outerHTML;
|
||||
}
|
||||
|
||||
// create marker with the icon
|
||||
this._mapZones.push(
|
||||
Leaflet.marker([latitude, longitude], {
|
||||
icon: Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className,
|
||||
}),
|
||||
interactive: this.interactiveZones,
|
||||
title,
|
||||
})
|
||||
);
|
||||
|
||||
// create circle around it
|
||||
const circle = Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: passive ? passiveZoneColor : zoneColor,
|
||||
radius,
|
||||
});
|
||||
this._mapZones.push(circle);
|
||||
|
||||
const marker = new DecoratedMarker([latitude, longitude], circle, {
|
||||
icon: Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className,
|
||||
}),
|
||||
interactive: this.interactiveZones,
|
||||
title,
|
||||
});
|
||||
|
||||
this._mapZones.push(marker);
|
||||
if (
|
||||
this.fitZones &&
|
||||
(typeof entity === "string" || entity.focus !== false)
|
||||
@@ -538,7 +553,7 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
// create marker with the icon
|
||||
const marker = Leaflet.marker([latitude, longitude], {
|
||||
const marker = new DecoratedMarker([latitude, longitude], undefined, {
|
||||
icon: Leaflet.divIcon({
|
||||
html: entityMarker,
|
||||
iconSize: [48, 48],
|
||||
@@ -546,24 +561,33 @@ export class HaMap extends ReactiveElement {
|
||||
}),
|
||||
title: title,
|
||||
});
|
||||
this._mapItems.push(marker);
|
||||
if (typeof entity === "string" || entity.focus !== false) {
|
||||
this._mapFocusItems.push(marker);
|
||||
}
|
||||
|
||||
// create circle around if entity has accuracy
|
||||
if (gpsAccuracy) {
|
||||
this._mapItems.push(
|
||||
Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: darkPrimaryColor,
|
||||
radius: gpsAccuracy,
|
||||
})
|
||||
);
|
||||
marker.decorationLayer = Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: darkPrimaryColor,
|
||||
radius: gpsAccuracy,
|
||||
});
|
||||
}
|
||||
|
||||
this._mapItems.push(marker);
|
||||
}
|
||||
|
||||
if (this.clusterMarkers) {
|
||||
this._mapCluster = Leaflet.markerClusterGroup({
|
||||
showCoverageOnHover: false,
|
||||
removeOutsideVisibleBounds: false,
|
||||
});
|
||||
this._mapCluster.addLayers(this._mapItems);
|
||||
map.addLayer(this._mapCluster);
|
||||
} else {
|
||||
this._mapItems.forEach((marker) => map.addLayer(marker));
|
||||
}
|
||||
|
||||
this._mapItems.forEach((marker) => map.addLayer(marker));
|
||||
this._mapZones.forEach((marker) => map.addLayer(marker));
|
||||
}
|
||||
|
||||
|
@@ -1,25 +1,81 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-icon";
|
||||
import "../ha-svg-icon";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
||||
export type TileIconImageStyle = "square" | "rounded-square" | "circle";
|
||||
|
||||
export const DEFAULT_TILE_ICON_BORDER_STYLE = "circle";
|
||||
|
||||
@customElement("ha-tile-icon")
|
||||
export class HaTileIcon extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public interactive = false;
|
||||
|
||||
@property({ attribute: "border-style", type: String })
|
||||
public imageStyle?: TileIconImageStyle;
|
||||
|
||||
@property({ attribute: false })
|
||||
public imageUrl?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="shape">
|
||||
if (this.imageUrl) {
|
||||
const imageStyle = this.imageStyle || DEFAULT_TILE_ICON_BORDER_STYLE;
|
||||
return html`
|
||||
<div class="container ${classMap({ [imageStyle]: this.imageUrl })}">
|
||||
<img alt="" src=${this.imageUrl} />
|
||||
</div>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="container ${this.interactive ? "background" : ""}">
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--tile-icon-color: var(--disabled-color);
|
||||
--mdc-icon-size: 22px;
|
||||
--tile-icon-opacity: 0.2;
|
||||
--tile-icon-hover-opacity: 0.35;
|
||||
--mdc-icon-size: 24px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
.shape::before {
|
||||
:host([interactive]:active) {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
:host([interactive]:hover) {
|
||||
--tile-icon-opacity: var(--tile-icon-hover-opacity);
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
}
|
||||
:host([interactive]:focus-visible) .container {
|
||||
box-shadow: 0 0 0 2px var(--tile-icon-color);
|
||||
}
|
||||
.container.rounded-square {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.container.square {
|
||||
border-radius: 0;
|
||||
}
|
||||
.container.background::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -27,24 +83,21 @@ export class HaTileIcon extends LitElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--tile-icon-color);
|
||||
transition: background-color 180ms ease-in-out;
|
||||
opacity: 0.2;
|
||||
transition:
|
||||
background-color 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
opacity: var(--tile-icon-opacity);
|
||||
}
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 180ms ease-in-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
.shape ::slotted(*) {
|
||||
.container ::slotted([slot="icon"]) {
|
||||
display: flex;
|
||||
color: var(--tile-icon-color);
|
||||
transition: color 180ms ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
.container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -1,53 +0,0 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
|
||||
export type TileImageStyle = "square" | "rounded-square" | "circle";
|
||||
@customElement("ha-tile-image")
|
||||
export class HaTileImage extends LitElement {
|
||||
@property({ attribute: false }) public imageUrl?: string;
|
||||
|
||||
@property({ attribute: false }) public imageAlt?: string;
|
||||
|
||||
@property({ attribute: false }) public imageStyle: TileImageStyle = "circle";
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="image ${this.imageStyle}">
|
||||
${this.imageUrl
|
||||
? html`<img alt=${ifDefined(this.imageAlt)} src=${this.imageUrl} />`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.image {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.image.rounded-square {
|
||||
border-radius: 8%;
|
||||
}
|
||||
.image.square {
|
||||
border-radius: 0;
|
||||
}
|
||||
.image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-tile-image": HaTileImage;
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ import type { HomeAssistant } from "../types";
|
||||
import { fileDownload } from "../util/file_download";
|
||||
import { domainToName } from "./integration";
|
||||
import type { FrontendLocaleData } from "./translation";
|
||||
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
|
||||
import checkValidDate from "../common/datetime/check_valid_date";
|
||||
import { handleFetchPromise } from "../util/hass-call-api";
|
||||
|
||||
@@ -130,7 +131,13 @@ export interface BackupContentExtended extends BackupContent, BackupData {}
|
||||
|
||||
export interface BackupInfo {
|
||||
backups: BackupContent[];
|
||||
backing_up: boolean;
|
||||
agent_errors: Record<string, string>;
|
||||
last_attempted_automatic_backup: string | null;
|
||||
last_completed_automatic_backup: string | null;
|
||||
last_non_idle_event: ManagerStateEvent | null;
|
||||
next_automatic_backup: string | null;
|
||||
next_automatic_backup_additional: boolean;
|
||||
state: BackupManagerState;
|
||||
}
|
||||
|
||||
export interface BackupDetails {
|
||||
|
@@ -233,11 +233,11 @@ export const restoreBackup = async (
|
||||
type: HassioBackupDetail["type"],
|
||||
backupSlug: string,
|
||||
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
||||
useSnapshotUrl: boolean
|
||||
useBackupUrl: boolean
|
||||
): Promise<void> => {
|
||||
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
||||
"POST",
|
||||
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
||||
`hassio/${useBackupUrl ? "backups" : "snapshots"}/${backupSlug}/restore/${type}`,
|
||||
backupDetails
|
||||
);
|
||||
};
|
||||
|
@@ -1,11 +1,5 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant, ThemeSettings } from "../types";
|
||||
import {
|
||||
fetchFrontendUserData,
|
||||
saveFrontendUserData,
|
||||
subscribeFrontendUserData,
|
||||
} from "./frontend";
|
||||
|
||||
export interface ThemeVars {
|
||||
// Incomplete
|
||||
@@ -56,16 +50,3 @@ export const subscribeThemes = (
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const SELECTED_THEME_KEY = "selectedTheme";
|
||||
|
||||
export const saveSelectedTheme = (hass: HomeAssistant, data?: ThemeSettings) =>
|
||||
saveFrontendUserData(hass.connection, SELECTED_THEME_KEY, data);
|
||||
|
||||
export const subscribeSelectedTheme = (
|
||||
hass: HomeAssistant,
|
||||
callback: (selectedTheme?: ThemeSettings | null) => void
|
||||
) => subscribeFrontendUserData(hass.connection, SELECTED_THEME_KEY, callback);
|
||||
|
||||
export const fetchSelectedTheme = (hass: HomeAssistant) =>
|
||||
fetchFrontendUserData(hass.connection, SELECTED_THEME_KEY);
|
||||
|
@@ -85,6 +85,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
|
||||
)
|
||||
) {
|
||||
this.navigateToResult = false;
|
||||
this._flowDone();
|
||||
showVoiceAssistantSetupDialog(this, {
|
||||
deviceId: devices[0].id,
|
||||
|
@@ -49,6 +49,8 @@ class LightRgbColorPicker extends LitElement {
|
||||
|
||||
@state() private _hsPickerValue?: [number, number];
|
||||
|
||||
@state() private _isInteracting?: boolean;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
@@ -211,7 +213,10 @@ class LightRgbColorPicker extends LitElement {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!changedProps.has("entityId") && !changedProps.has("hass")) {
|
||||
if (
|
||||
this._isInteracting ||
|
||||
(!changedProps.has("entityId") && !changedProps.has("hass"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -219,10 +224,13 @@ class LightRgbColorPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _hsColorCursorMoved(ev: CustomEvent) {
|
||||
if (!ev.detail.value) {
|
||||
const color = ev.detail.value;
|
||||
this._isInteracting = color !== undefined;
|
||||
|
||||
if (color === undefined) {
|
||||
return;
|
||||
}
|
||||
this._hsPickerValue = ev.detail.value;
|
||||
this._hsPickerValue = color;
|
||||
|
||||
this._throttleUpdateColor();
|
||||
}
|
||||
|
@@ -22,7 +22,6 @@ import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity_attributes";
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"color-changed": LightColor;
|
||||
"color-hovered": LightColor | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +53,8 @@ class LightColorTempPicker extends LitElement {
|
||||
|
||||
@state() private _ctPickerValue?: number;
|
||||
|
||||
@state() private _isInteracting?: boolean;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
@@ -113,7 +114,7 @@ class LightColorTempPicker extends LitElement {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!changedProps.has("stateObj")) {
|
||||
if (this._isInteracting || !changedProps.has("stateObj")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,16 +124,14 @@ class LightColorTempPicker extends LitElement {
|
||||
private _ctColorCursorMoved(ev: CustomEvent) {
|
||||
const ct = ev.detail.value;
|
||||
|
||||
this._isInteracting = ct !== undefined;
|
||||
|
||||
if (isNaN(ct) || this._ctPickerValue === ct) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._ctPickerValue = ct;
|
||||
|
||||
fireEvent(this, "color-hovered", {
|
||||
color_temp_kelvin: ct,
|
||||
});
|
||||
|
||||
this._throttleUpdateColorTemp();
|
||||
}
|
||||
|
||||
@@ -143,8 +142,6 @@ class LightColorTempPicker extends LitElement {
|
||||
private _ctColorChanged(ev: CustomEvent) {
|
||||
const ct = ev.detail.value;
|
||||
|
||||
fireEvent(this, "color-hovered", undefined);
|
||||
|
||||
if (isNaN(ct) || this._ctPickerValue === ct) {
|
||||
return;
|
||||
}
|
||||
|
@@ -20,12 +20,6 @@
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<%= renderTemplate("_style_base.html.template") %>
|
||||
<style>
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
color: var(--primary-text-color, #212121);
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiAlertCircleCheck,
|
||||
mdiArrowDown,
|
||||
@@ -27,7 +26,9 @@ import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -240,89 +241,104 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
</div> `
|
||||
: nothing}
|
||||
|
||||
<ha-button-menu
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-md-menu-item .clickAction=${this._runAction}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.run"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${!this._uiModeAvailable}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this._uiModeAvailable}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.action.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
@@ -331,15 +347,15 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${this.action.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -347,11 +363,11 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -424,47 +440,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._runAction();
|
||||
break;
|
||||
case 1:
|
||||
await this._renameAction();
|
||||
break;
|
||||
case 2:
|
||||
fireEvent(this, "duplicate");
|
||||
break;
|
||||
case 3:
|
||||
this._setClipboard();
|
||||
break;
|
||||
case 4:
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
break;
|
||||
case 5:
|
||||
fireEvent(this, "move-up");
|
||||
break;
|
||||
case 6:
|
||||
fireEvent(this, "move-down");
|
||||
break;
|
||||
case 7:
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
break;
|
||||
case 8:
|
||||
this._onDisable();
|
||||
break;
|
||||
case 9:
|
||||
this._onDelete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
this._clipboard = {
|
||||
...this._clipboard,
|
||||
@@ -472,16 +447,16 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _onDisable() {
|
||||
private _onDisable = () => {
|
||||
const enabled = !(this.action.enabled ?? true);
|
||||
const value = { ...this.action, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
if (this._yamlMode) {
|
||||
this._yamlEditor?.setValue(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private async _runAction() {
|
||||
private _runAction = async () => {
|
||||
const validated = await validateConfig(this.hass, {
|
||||
actions: this.action,
|
||||
});
|
||||
@@ -513,9 +488,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.run_action_success"
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _onDelete() {
|
||||
private _onDelete = () => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete_confirm_title"
|
||||
@@ -530,7 +505,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
@@ -561,7 +536,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
private async _renameAction(): Promise<void> {
|
||||
private _renameAction = async (): Promise<void> => {
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.change_alias"
|
||||
@@ -598,7 +573,37 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this._yamlEditor?.setValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _duplicateAction = () => {
|
||||
fireEvent(this, "duplicate");
|
||||
};
|
||||
|
||||
private _copyAction = () => {
|
||||
this._setClipboard();
|
||||
};
|
||||
|
||||
private _cutAction = () => {
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
};
|
||||
|
||||
private _moveUp = () => {
|
||||
fireEvent(this, "move-up");
|
||||
};
|
||||
|
||||
private _moveDown = () => {
|
||||
fireEvent(this, "move-down");
|
||||
};
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
};
|
||||
|
||||
public expand() {
|
||||
this.updateComplete.then(() => {
|
||||
@@ -610,7 +615,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-button-menu,
|
||||
ha-icon-button {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
@@ -649,18 +653,11 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
|
||||
mwc-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
mwc-list-item.hidden {
|
||||
display: none;
|
||||
}
|
||||
.warning ul {
|
||||
margin: 4px 0;
|
||||
}
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@@ -24,11 +23,12 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
@@ -141,12 +141,12 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-button-menu
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -155,76 +155,91 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-md-menu-item .clickAction=${this._testCondition}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.test"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this._warnings}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.condition.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
@@ -233,15 +248,15 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${this.condition.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -249,11 +264,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -325,47 +340,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
await this._testCondition();
|
||||
break;
|
||||
case 1:
|
||||
await this._renameCondition();
|
||||
break;
|
||||
case 2:
|
||||
fireEvent(this, "duplicate");
|
||||
break;
|
||||
case 3:
|
||||
this._setClipboard();
|
||||
break;
|
||||
case 4:
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
break;
|
||||
case 5:
|
||||
fireEvent(this, "move-up");
|
||||
break;
|
||||
case 6:
|
||||
fireEvent(this, "move-down");
|
||||
break;
|
||||
case 7:
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
break;
|
||||
case 8:
|
||||
this._onDisable();
|
||||
break;
|
||||
case 9:
|
||||
this._onDelete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
this._clipboard = {
|
||||
...this._clipboard,
|
||||
@@ -373,13 +347,13 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _onDisable() {
|
||||
private _onDisable = () => {
|
||||
const enabled = !(this.condition.enabled ?? true);
|
||||
const value = { ...this.condition, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
};
|
||||
|
||||
private _onDelete() {
|
||||
private _onDelete = () => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.delete_confirm_title"
|
||||
@@ -394,7 +368,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _switchUiMode() {
|
||||
this._warnings = undefined;
|
||||
@@ -406,7 +380,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
private async _testCondition() {
|
||||
private _testCondition = async () => {
|
||||
if (this._testing) {
|
||||
return;
|
||||
}
|
||||
@@ -461,9 +435,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this._testing = false;
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private async _renameCondition(): Promise<void> {
|
||||
private _renameCondition = async (): Promise<void> => {
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.change_alias"
|
||||
@@ -489,7 +463,37 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _duplicateCondition = () => {
|
||||
fireEvent(this, "duplicate");
|
||||
};
|
||||
|
||||
private _copyCondition = () => {
|
||||
this._setClipboard();
|
||||
};
|
||||
|
||||
private _cutCondition = () => {
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
};
|
||||
|
||||
private _moveUp = () => {
|
||||
fireEvent(this, "move-up");
|
||||
};
|
||||
|
||||
private _moveDown = () => {
|
||||
fireEvent(this, "move-down");
|
||||
};
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
};
|
||||
|
||||
public expand() {
|
||||
this.updateComplete.then(() => {
|
||||
@@ -501,9 +505,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-button-menu {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
@@ -539,12 +540,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
ha-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-list-item.hidden {
|
||||
display: none;
|
||||
}
|
||||
.testing {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
@@ -571,8 +566,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
.testing.pass {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -339,9 +339,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
hidden: narrow,
|
||||
title: "",
|
||||
type: "overflow",
|
||||
label: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||
title: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||
template: (automation) => html`
|
||||
<ha-entity-toggle
|
||||
.stateObj=${automation}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@@ -28,7 +27,9 @@ import { capitalizeFirstLetter } from "../../../../common/string/capitalize-firs
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -169,12 +170,12 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-button-menu
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -182,84 +183,93 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameTrigger}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._showTriggerId}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${!supported}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!supported}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
@@ -270,16 +280,16 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${"enabled" in this.trigger &&
|
||||
this.trigger.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDelete}
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -287,11 +297,11 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -464,48 +474,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
await this._renameTrigger();
|
||||
break;
|
||||
case 1:
|
||||
this._requestShowId = true;
|
||||
this.expand();
|
||||
break;
|
||||
case 2:
|
||||
fireEvent(this, "duplicate");
|
||||
break;
|
||||
case 3:
|
||||
this._setClipboard();
|
||||
break;
|
||||
case 4:
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
break;
|
||||
case 5:
|
||||
fireEvent(this, "move-up");
|
||||
break;
|
||||
case 6:
|
||||
fireEvent(this, "move-down");
|
||||
break;
|
||||
case 7:
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
break;
|
||||
case 8:
|
||||
this._onDisable();
|
||||
break;
|
||||
case 9:
|
||||
this._onDelete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
this._clipboard = {
|
||||
...this._clipboard,
|
||||
@@ -513,7 +481,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _onDelete() {
|
||||
private _onDelete = () => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.delete_confirm_title"
|
||||
@@ -528,9 +496,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _onDisable() {
|
||||
private _onDisable = () => {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const enabled = !(this.trigger.enabled ?? true);
|
||||
const value = { ...this.trigger, enabled };
|
||||
@@ -538,7 +506,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
if (this._yamlMode) {
|
||||
this._yamlEditor?.setValue(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _idChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
@@ -605,7 +573,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _renameTrigger(): Promise<void> {
|
||||
private _renameTrigger = async (): Promise<void> => {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
@@ -636,7 +604,42 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
this._yamlEditor?.setValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _showTriggerId = () => {
|
||||
this._requestShowId = true;
|
||||
this.expand();
|
||||
};
|
||||
|
||||
private _duplicateTrigger = () => {
|
||||
fireEvent(this, "duplicate");
|
||||
};
|
||||
|
||||
private _copyTrigger = () => {
|
||||
this._setClipboard();
|
||||
};
|
||||
|
||||
private _cutTrigger = () => {
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
};
|
||||
|
||||
private _moveUp = () => {
|
||||
fireEvent(this, "move-up");
|
||||
};
|
||||
|
||||
private _moveDown = () => {
|
||||
fireEvent(this, "move-down");
|
||||
};
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
};
|
||||
|
||||
public expand() {
|
||||
this.updateComplete.then(() => {
|
||||
@@ -648,9 +651,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-button-menu {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
@@ -714,18 +714,12 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
background-color: var(--accent-color);
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
ha-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-list-item.hidden {
|
||||
display: none;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
@@ -22,7 +23,6 @@ import {
|
||||
import type { CloudStatus } from "../../../../../data/cloud";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { brandsUrl } from "../../../../../util/brands-url";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
|
||||
const DEFAULT_AGENTS = [];
|
||||
|
||||
|
@@ -46,7 +46,7 @@ enum BackupScheduleTime {
|
||||
}
|
||||
|
||||
interface RetentionData {
|
||||
type: "copies" | "days";
|
||||
type: "copies" | "days" | "forever";
|
||||
value: number;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ const RETENTION_PRESETS: Record<
|
||||
RetentionData
|
||||
> = {
|
||||
copies_3: { type: "copies", value: 3 },
|
||||
forever: { type: "days", value: 0 },
|
||||
forever: { type: "forever", value: 0 },
|
||||
};
|
||||
|
||||
const SCHEDULE_OPTIONS = [
|
||||
@@ -79,7 +79,10 @@ const computeRetentionPreset = (
|
||||
data: RetentionData
|
||||
): RetentionPreset | undefined => {
|
||||
for (const [key, value] of Object.entries(RETENTION_PRESETS)) {
|
||||
if (value.type === data.type && value.value === data.value) {
|
||||
if (
|
||||
value.type === data.type &&
|
||||
(value.type === RetentionPreset.FOREVER || value.value === data.value)
|
||||
) {
|
||||
return key as RetentionPreset;
|
||||
}
|
||||
}
|
||||
@@ -92,7 +95,7 @@ interface FormData {
|
||||
time?: string | null;
|
||||
days: BackupDay[];
|
||||
retention: {
|
||||
type: "copies" | "days";
|
||||
type: "copies" | "days" | "forever";
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
@@ -142,7 +145,12 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
? config.schedule.days
|
||||
: [],
|
||||
retention: {
|
||||
type: config.retention.days != null ? "days" : "copies",
|
||||
type:
|
||||
config.retention.days === null && config.retention.copies === null
|
||||
? "forever"
|
||||
: config.retention.days != null
|
||||
? "days"
|
||||
: "copies",
|
||||
value: config.retention.days ?? config.retention.copies ?? 3,
|
||||
},
|
||||
};
|
||||
@@ -160,9 +168,11 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
: [],
|
||||
},
|
||||
retention:
|
||||
data.retention.type === "days"
|
||||
? { days: data.retention.value, copies: null }
|
||||
: { copies: data.retention.value, days: null },
|
||||
data.retention.type === "forever"
|
||||
? { days: null, copies: null }
|
||||
: data.retention.type === "days"
|
||||
? { days: data.retention.value, copies: null }
|
||||
: { copies: data.retention.value, days: null },
|
||||
};
|
||||
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
@@ -481,9 +491,19 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
private _retentionPresetChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const target = ev.currentTarget as HaMdSelect;
|
||||
const value = target.value as RetentionPreset;
|
||||
let value = target.value as RetentionPreset;
|
||||
|
||||
// custom needs to have a type of days or copies, set it to default copies 3
|
||||
if (
|
||||
value === RetentionPreset.CUSTOM &&
|
||||
this._retentionPreset === RetentionPreset.FOREVER
|
||||
) {
|
||||
this._retentionPreset = value;
|
||||
value = RetentionPreset.COPIES_3;
|
||||
} else {
|
||||
this._retentionPreset = value;
|
||||
}
|
||||
|
||||
this._retentionPreset = value;
|
||||
if (value !== RetentionPreset.CUSTOM) {
|
||||
const data = this._getData(this.value);
|
||||
const retention = RETENTION_PRESETS[value];
|
||||
@@ -493,7 +513,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
}
|
||||
this._setData({
|
||||
...data,
|
||||
retention: RETENTION_PRESETS[value],
|
||||
retention,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -504,6 +524,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
const value = parseInt(target.value);
|
||||
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
|
||||
const data = this._getData(this.value);
|
||||
target.value = clamped.toString();
|
||||
this._setData({
|
||||
...data,
|
||||
retention: {
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
mdiUpload,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-icon";
|
||||
@@ -460,7 +461,17 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
extended
|
||||
@click=${this._newBackup}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
${backupInProgress
|
||||
? html`<div slot="icon">
|
||||
<ha-circular-progress
|
||||
.size=${"small"}
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>`}
|
||||
</ha-fab>
|
||||
`
|
||||
: nothing}
|
||||
@@ -605,7 +616,14 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return haStyle;
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-circular-progress {
|
||||
--md-sys-color-primary: var(--mdc-theme-on-secondary);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,7 @@ import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
@@ -17,8 +18,10 @@ import type {
|
||||
BackupAgent,
|
||||
BackupConfig,
|
||||
BackupContent,
|
||||
BackupInfo,
|
||||
} from "../../../data/backup";
|
||||
import {
|
||||
computeBackupAgentName,
|
||||
generateBackup,
|
||||
generateBackupWithAutomaticSettings,
|
||||
} from "../../../data/backup";
|
||||
@@ -50,6 +53,8 @@ class HaConfigBackupOverview extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public manager!: ManagerStateEvent;
|
||||
|
||||
@property({ attribute: false }) public info?: BackupInfo;
|
||||
|
||||
@property({ attribute: false }) public backups: BackupContent[] = [];
|
||||
|
||||
@property({ attribute: false }) public fetching = false;
|
||||
@@ -151,6 +156,26 @@ class HaConfigBackupOverview extends LitElement {
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<div class="content">
|
||||
${this.info && Object.keys(this.info.agent_errors).length
|
||||
? html`${Object.entries(this.info.agent_errors).map(
|
||||
([agentId, error]) =>
|
||||
html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.backup.overview.agent_error",
|
||||
{
|
||||
name: computeBackupAgentName(
|
||||
this.hass.localize,
|
||||
agentId,
|
||||
this.agents
|
||||
),
|
||||
}
|
||||
)}
|
||||
>
|
||||
${error}
|
||||
</ha-alert>`
|
||||
)}`
|
||||
: nothing}
|
||||
${backupInProgress
|
||||
? html`
|
||||
<ha-backup-overview-progress
|
||||
@@ -204,7 +229,14 @@ class HaConfigBackupOverview extends LitElement {
|
||||
extended
|
||||
@click=${this._newBackup}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
${backupInProgress
|
||||
? html`<div slot="icon">
|
||||
<ha-circular-progress
|
||||
.size=${"small"}
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>`}
|
||||
</ha-fab>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -231,6 +263,9 @@ class HaConfigBackupOverview extends LitElement {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
ha-circular-progress {
|
||||
--md-sys-color-primary: var(--mdc-theme-on-secondary);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { mdiDotsVertical, mdiHarddisk } from "@mdi/js";
|
||||
import { mdiDotsVertical, mdiHarddisk, mdiOpenInNew } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -28,6 +28,7 @@ import "./components/config/ha-backup-config-encryption-key";
|
||||
import "./components/config/ha-backup-config-schedule";
|
||||
import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule";
|
||||
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
|
||||
@customElement("ha-config-backup-settings")
|
||||
class HaConfigBackupSettings extends LitElement {
|
||||
@@ -98,6 +99,8 @@ class HaConfigBackupSettings extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supervisor = isComponentLoaded(this.hass, "hassio");
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/backup"
|
||||
@@ -105,7 +108,7 @@ class HaConfigBackupSettings extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.backup.settings.header")}
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio")
|
||||
${supervisor
|
||||
? html`
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
@@ -203,6 +206,29 @@ class HaConfigBackupSettings extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/integrations/#backup")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-button>
|
||||
<ha-svg-icon slot="icon" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.locations.more_locations"
|
||||
)}
|
||||
</ha-button>
|
||||
</a>
|
||||
${supervisor
|
||||
? html`<a href="/config/storage">
|
||||
<ha-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.locations.manage_network_storage"
|
||||
)}
|
||||
</ha-button>
|
||||
</a>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
@@ -342,6 +368,9 @@ class HaConfigBackupSettings extends LitElement {
|
||||
.card-content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import type {
|
||||
BackupAgent,
|
||||
BackupConfig,
|
||||
BackupContent,
|
||||
BackupInfo,
|
||||
} from "../../../data/backup";
|
||||
import {
|
||||
compareAgents,
|
||||
@@ -44,7 +44,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
|
||||
@state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE;
|
||||
|
||||
@state() private _backups: BackupContent[] = [];
|
||||
@state() private _info?: BackupInfo;
|
||||
|
||||
@state() private _agents: BackupAgent[] = [];
|
||||
|
||||
@@ -87,8 +87,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
}
|
||||
|
||||
private async _fetchBackupInfo() {
|
||||
const info = await fetchBackupInfo(this.hass);
|
||||
this._backups = info.backups;
|
||||
this._info = await fetchBackupInfo(this.hass);
|
||||
}
|
||||
|
||||
private async _fetchBackupConfig() {
|
||||
@@ -134,7 +133,8 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.cloudStatus = this.cloudStatus;
|
||||
pageEl.manager = this._manager;
|
||||
pageEl.backups = this._backups;
|
||||
pageEl.info = this._info;
|
||||
pageEl.backups = this._info?.backups || [];
|
||||
pageEl.config = this._config;
|
||||
pageEl.agents = this._agents;
|
||||
pageEl.fetching = this._fetching;
|
||||
|
@@ -355,6 +355,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
showNarrow: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
minWidth: "80px",
|
||||
maxWidth: "80px",
|
||||
template: (entry) =>
|
||||
entry.unavailable ||
|
||||
entry.disabled_by ||
|
||||
|
@@ -346,9 +346,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
groupable: true,
|
||||
},
|
||||
editable: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.helpers.picker.headers.editable"),
|
||||
title: localize("ui.panel.config.helpers.picker.headers.editable"),
|
||||
type: "icon",
|
||||
sortable: true,
|
||||
minWidth: "88px",
|
||||
maxWidth: "88px",
|
||||
showNarrow: true,
|
||||
template: (helper) => html`
|
||||
${!helper.editable
|
||||
|
@@ -70,7 +70,7 @@ export class HaConfigFlowCard extends LitElement {
|
||||
? html`<a
|
||||
href=${this.flow.context.configuration_url.replace(
|
||||
/^homeassistant:\/\//,
|
||||
""
|
||||
"/"
|
||||
)}
|
||||
rel="noreferrer"
|
||||
target=${this.flow.context.configuration_url.startsWith(
|
||||
|
@@ -76,6 +76,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
@state()
|
||||
private _statistics?: ZWaveJSControllerStatisticsUpdatedMessage;
|
||||
|
||||
private _dialogOpen = false;
|
||||
|
||||
protected async firstUpdated() {
|
||||
if (this.hass) {
|
||||
await this._fetchData();
|
||||
@@ -104,11 +106,17 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
),
|
||||
subscribeS2Inclusion(this.hass, this.configEntryId, (message) => {
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: this.configEntryId,
|
||||
dsk: message.dsk,
|
||||
onStop: () => setTimeout(() => this._fetchData(), 100),
|
||||
});
|
||||
if (!this._dialogOpen) {
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: this.configEntryId,
|
||||
dsk: message.dsk,
|
||||
onStop: () => {
|
||||
setTimeout(() => this._fetchData(), 100);
|
||||
this._dialogOpen = false;
|
||||
},
|
||||
});
|
||||
this._dialogOpen = true;
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -570,11 +578,17 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _addNodeClicked() {
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: this.configEntryId!,
|
||||
// refresh the data after the dialog is closed. add a small delay for the inclusion state to update
|
||||
onStop: () => setTimeout(() => this._fetchData(), 100),
|
||||
});
|
||||
if (!this._dialogOpen) {
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: this.configEntryId!,
|
||||
// refresh the data after the dialog is closed. add a small delay for the inclusion state to update
|
||||
onStop: () => {
|
||||
setTimeout(() => this._fetchData(), 100);
|
||||
this._dialogOpen = false;
|
||||
},
|
||||
});
|
||||
this._dialogOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async _removeNodeClicked() {
|
||||
|
@@ -706,7 +706,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
--data-table-border-width: 0;
|
||||
}
|
||||
:host(:not([narrow])) ha-data-table {
|
||||
height: calc(100vh - 1px - var(--header-height));
|
||||
height: calc(100vh - 1px - var(--header-height) - 48px);
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
@@ -20,40 +20,31 @@ export class HuiCardFeatures extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<div class="container">
|
||||
${this.features.map(
|
||||
(feature) => html`
|
||||
<hui-card-feature
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.color=${this.color}
|
||||
.feature=${feature}
|
||||
></hui-card-feature>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${this.features.map(
|
||||
(feature) => html`
|
||||
<hui-card-feature
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.color=${this.color}
|
||||
.feature=${feature}
|
||||
></hui-card-feature>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--feature-color: var(--state-icon-color);
|
||||
--feature-padding: 12px;
|
||||
--feature-height: 42px;
|
||||
--feature-border-radius: 12px;
|
||||
--feature-button-spacing: 12px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--feature-padding);
|
||||
padding-top: 0px;
|
||||
gap: var(--feature-padding);
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
@@ -327,17 +327,19 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
);
|
||||
|
||||
const untrackedConsumption: BarSeriesOption["data"] = [];
|
||||
Object.keys(consumptionData.total).forEach((time) => {
|
||||
const ts = Number(time);
|
||||
const value =
|
||||
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
|
||||
const dataPoint: number[] = [ts, value];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
dataPoint[0] = compareTransform(new Date(ts)).getTime();
|
||||
}
|
||||
untrackedConsumption.push(dataPoint);
|
||||
});
|
||||
Object.keys(consumptionData.total)
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.forEach((time) => {
|
||||
const ts = Number(time);
|
||||
const value =
|
||||
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
|
||||
const dataPoint: number[] = [ts, value];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
dataPoint[0] = compareTransform(new Date(ts)).getTime();
|
||||
}
|
||||
untrackedConsumption.push(dataPoint);
|
||||
});
|
||||
// random id to always add untracked at the end
|
||||
const order = Date.now();
|
||||
const dataset: BarSeriesOption = {
|
||||
@@ -448,7 +450,15 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
});
|
||||
});
|
||||
return sorted_devices
|
||||
.map((device) => data.find((d) => (d.id as string).includes(device))!)
|
||||
.map(
|
||||
(device) =>
|
||||
data.find((d) => {
|
||||
const id = (d.id as string)
|
||||
.replace(/^compare-/, "") // Remove compare- prefix
|
||||
.replace(/-\d+$/, ""); // Remove numeric suffix
|
||||
return id === device;
|
||||
})!
|
||||
)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
|
@@ -256,6 +256,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
hui-card-features {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
padding: 0 12px 12px 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import { mdiImageFilterCenterFocus } from "@mdi/js";
|
||||
import {
|
||||
mdiDotsHexagon,
|
||||
mdiGoogleCirclesCommunities,
|
||||
mdiImageFilterCenterFocus,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { LatLngTuple } from "leaflet";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -72,6 +76,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _error?: { code: string; message: string };
|
||||
|
||||
@state() private _clusterMarkers = true;
|
||||
|
||||
private _subscribed?: Promise<(() => Promise<void>) | undefined>;
|
||||
|
||||
public setConfig(config: MapCardConfig): void {
|
||||
@@ -170,18 +176,32 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
.autoFit=${this._config.auto_fit || false}
|
||||
.fitZones=${this._config.fit_zones}
|
||||
.themeMode=${themeMode}
|
||||
.clusterMarkers=${this._clusterMarkers}
|
||||
interactive-zones
|
||||
render-passive
|
||||
></ha-map>
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.map.reset_focus"
|
||||
)}
|
||||
.path=${mdiImageFilterCenterFocus}
|
||||
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
|
||||
@click=${this._fitMap}
|
||||
tabindex="0"
|
||||
></ha-icon-button>
|
||||
<div id="buttons">
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.map.toggle_grouping"
|
||||
)}
|
||||
.path=${this._clusterMarkers
|
||||
? mdiGoogleCirclesCommunities
|
||||
: mdiDotsHexagon}
|
||||
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
|
||||
@click=${this._toggleClusterMarkers}
|
||||
tabindex="0"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.map.reset_focus"
|
||||
)}
|
||||
.path=${mdiImageFilterCenterFocus}
|
||||
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
|
||||
@click=${this._fitMap}
|
||||
tabindex="0"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -320,6 +340,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
this._map?.fitMap();
|
||||
}
|
||||
|
||||
private _toggleClusterMarkers() {
|
||||
this._clusterMarkers = !this._clusterMarkers;
|
||||
}
|
||||
|
||||
private _getColor(entityId: string): string {
|
||||
let color = this._colorDict[entityId];
|
||||
if (color) {
|
||||
@@ -464,11 +488,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
#buttons {
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
left: 3px;
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#root {
|
||||
|
@@ -3,17 +3,21 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import hash from "object-hash";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-alert";
|
||||
import type { RenderTemplateResult } from "../../../data/ws-templates";
|
||||
import { subscribeRenderTemplate } from "../../../data/ws-templates";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { CacheManager } from "../../../util/cache-manager";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { MarkdownCardConfig } from "./types";
|
||||
|
||||
const templateCache = new CacheManager<RenderTemplateResult>(1000);
|
||||
|
||||
@customElement("hui-markdown-card")
|
||||
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@@ -68,9 +72,32 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
this._tryConnect();
|
||||
}
|
||||
|
||||
private _computeCacheKey() {
|
||||
return hash(this._config);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._tryDisconnect();
|
||||
|
||||
if (this._config && this._templateResult) {
|
||||
const key = this._computeCacheKey();
|
||||
templateCache.set(key, this._templateResult);
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
super.willUpdate(_changedProperties);
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._templateResult) {
|
||||
const key = this._computeCacheKey();
|
||||
if (templateCache.has(key)) {
|
||||
this._templateResult = templateCache.get(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -87,6 +114,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
: nothing}
|
||||
<ha-card .header=${this._config.title}>
|
||||
<ha-markdown
|
||||
cache
|
||||
breaks
|
||||
class=${classMap({
|
||||
"no-header": !this._config.title,
|
||||
@@ -107,7 +135,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
this._tryConnect();
|
||||
}
|
||||
const shouldBeHidden =
|
||||
this._templateResult &&
|
||||
!!this._templateResult &&
|
||||
this._config.show_empty === false &&
|
||||
this._templateResult.result.length === 0;
|
||||
if (shouldBeHidden !== this.hidden) {
|
||||
|
@@ -327,7 +327,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
const endDate = this._energyEnd;
|
||||
try {
|
||||
let unitClass;
|
||||
let unitClass: string | undefined | null;
|
||||
if (this._config!.unit && this._metadata) {
|
||||
const metadata = Object.values(this._metadata).find(
|
||||
(metaData) =>
|
||||
|
@@ -248,6 +248,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
hui-card-features {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
padding: 0 12px 12px 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -18,8 +18,7 @@ import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/tile/ha-tile-badge";
|
||||
import "../../../components/tile/ha-tile-icon";
|
||||
import "../../../components/tile/ha-tile-image";
|
||||
import type { TileImageStyle } from "../../../components/tile/ha-tile-image";
|
||||
import type { TileIconImageStyle } from "../../../components/tile/ha-tile-icon";
|
||||
import "../../../components/tile/ha-tile-info";
|
||||
import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
@@ -36,7 +35,7 @@ import type {
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import { renderTileBadge } from "./tile/badges/tile-badge";
|
||||
import type { ThermostatCardConfig, TileCardConfig } from "./types";
|
||||
import type { TileCardConfig } from "./types";
|
||||
|
||||
export const getEntityDefaultTileIconAction = (entityId: string) => {
|
||||
const domain = computeDomain(entityId);
|
||||
@@ -44,10 +43,10 @@ export const getEntityDefaultTileIconAction = (entityId: string) => {
|
||||
DOMAINS_TOGGLE.has(domain) ||
|
||||
["button", "input_button", "scene"].includes(domain);
|
||||
|
||||
return supportsIconAction ? "toggle" : "more-info";
|
||||
return supportsIconAction ? "toggle" : "none";
|
||||
};
|
||||
|
||||
const DOMAIN_IMAGE_STYLE: Record<string, TileImageStyle> = {
|
||||
const DOMAIN_IMAGE_SHAPE: Record<string, TileIconImageStyle> = {
|
||||
update: "square",
|
||||
media_player: "rounded-square",
|
||||
};
|
||||
@@ -84,7 +83,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _config?: TileCardConfig;
|
||||
|
||||
public setConfig(config: ThermostatCardConfig): void {
|
||||
public setConfig(config: TileCardConfig): void {
|
||||
if (!config.entity) {
|
||||
throw new Error("Specify an entity");
|
||||
}
|
||||
@@ -101,10 +100,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
const featuresPosition =
|
||||
this._config && this._featurePosition(this._config);
|
||||
const featuresCount = this._config?.features?.length || 0;
|
||||
return (
|
||||
1 +
|
||||
(this._config?.vertical ? 1 : 0) +
|
||||
(this._config?.features?.length || 0)
|
||||
(featuresPosition === "inline" ? 0 : featuresCount)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,9 +114,16 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
const columns = 6;
|
||||
let min_columns = 6;
|
||||
let rows = 1;
|
||||
if (this._config?.features?.length) {
|
||||
rows += this._config.features.length;
|
||||
const featurePosition = this._config && this._featurePosition(this._config);
|
||||
const featuresCount = this._config?.features?.length || 0;
|
||||
if (featuresCount) {
|
||||
if (featurePosition === "inline") {
|
||||
min_columns = 12;
|
||||
} else {
|
||||
rows += featuresCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._config?.vertical) {
|
||||
rows++;
|
||||
min_columns = 3;
|
||||
@@ -196,7 +205,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
);
|
||||
|
||||
get hasCardAction() {
|
||||
private get _hasCardAction() {
|
||||
return (
|
||||
!this._config?.tap_action ||
|
||||
hasAction(this._config?.tap_action) ||
|
||||
@@ -205,12 +214,29 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
}
|
||||
|
||||
get hasIconAction() {
|
||||
private get _hasIconAction() {
|
||||
return (
|
||||
!this._config?.icon_tap_action || hasAction(this._config?.icon_tap_action)
|
||||
);
|
||||
}
|
||||
|
||||
private _featurePosition = memoizeOne((config: TileCardConfig) => {
|
||||
if (config.vertical) {
|
||||
return "bottom";
|
||||
}
|
||||
return config.features_position || "bottom";
|
||||
});
|
||||
|
||||
private _displayedFeatures = memoizeOne((config: TileCardConfig) => {
|
||||
const features = config.features || [];
|
||||
const featurePosition = this._featurePosition(config);
|
||||
|
||||
if (featurePosition === "inline") {
|
||||
return features.slice(0, 1);
|
||||
}
|
||||
return features;
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this.hass) {
|
||||
return nothing;
|
||||
@@ -224,14 +250,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<div class="icon-container">
|
||||
<ha-tile-icon>
|
||||
<ha-svg-icon .path=${mdiHelp}></ha-svg-icon>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-icon>
|
||||
<ha-svg-icon slot="icon" .path=${mdiHelp}></ha-svg-icon>
|
||||
<ha-tile-badge class="not-found">
|
||||
<ha-svg-icon .path=${mdiExclamationThick}></ha-svg-icon>
|
||||
</ha-tile-badge>
|
||||
</div>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info
|
||||
.primary=${entityId}
|
||||
secondary=${this.hass.localize("ui.card.tile.not_found")}
|
||||
@@ -266,6 +290,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
? this._getImageUrl(stateObj)
|
||||
: undefined;
|
||||
|
||||
const featurePosition = this._featurePosition(this._config);
|
||||
const features = this._displayedFeatures(this._config);
|
||||
|
||||
const containerOrientationClass =
|
||||
featurePosition === "inline" ? "horizontal" : "";
|
||||
|
||||
return html`
|
||||
<ha-card style=${styleMap(style)} class=${classMap({ active })}>
|
||||
<div
|
||||
@@ -275,58 +305,49 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
role=${ifDefined(this.hasCardAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this.hasCardAction ? "0" : undefined)}
|
||||
role=${ifDefined(this._hasCardAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this._hasCardAction ? "0" : undefined)}
|
||||
aria-labelledby="info"
|
||||
>
|
||||
<ha-ripple .disabled=${!this.hasCardAction}></ha-ripple>
|
||||
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="container ${containerOrientationClass}">
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<div
|
||||
class="icon-container"
|
||||
role=${ifDefined(this.hasIconAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this.hasIconAction ? "0" : undefined)}
|
||||
<ha-tile-icon
|
||||
role=${ifDefined(this._hasIconAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this._hasIconAction ? "0" : undefined)}
|
||||
@action=${this._handleIconAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.icon_hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.icon_double_tap_action),
|
||||
})}
|
||||
.interactive=${this._hasIconAction}
|
||||
.imageStyle=${DOMAIN_IMAGE_SHAPE[domain]}
|
||||
.imageUrl=${imageUrl}
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
>
|
||||
${imageUrl
|
||||
? html`
|
||||
<ha-tile-image
|
||||
.imageStyle=${DOMAIN_IMAGE_STYLE[domain] || "circle"}
|
||||
.imageUrl=${imageUrl}
|
||||
></ha-tile-image>
|
||||
`
|
||||
: html`
|
||||
<ha-tile-icon
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
>
|
||||
<ha-state-icon
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
</ha-tile-icon>
|
||||
`}
|
||||
<ha-state-icon
|
||||
slot="icon"
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
${renderTileBadge(stateObj, this.hass)}
|
||||
</div>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info
|
||||
id="info"
|
||||
.primary=${name}
|
||||
.secondary=${stateDisplay}
|
||||
></ha-tile-info>
|
||||
</div>
|
||||
${this._config.features
|
||||
${features.length > 0
|
||||
? html`
|
||||
<hui-card-features
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.color=${this._config.color}
|
||||
.features=${this._config.features}
|
||||
.features=${features}
|
||||
></hui-card-features>
|
||||
`
|
||||
: nothing}
|
||||
@@ -363,6 +384,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
[role="button"]:focus {
|
||||
outline: none;
|
||||
@@ -383,6 +405,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
.container.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -392,53 +418,35 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.container.horizontal .content {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.vertical {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.vertical .icon-container {
|
||||
margin-bottom: 10px;
|
||||
margin-right: 0;
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.vertical ha-tile-info {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
.icon-container {
|
||||
position: relative;
|
||||
flex: none;
|
||||
margin-right: 10px;
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 10px;
|
||||
direction: var(--direction);
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
.icon-container ha-tile-icon,
|
||||
.icon-container ha-tile-image {
|
||||
ha-tile-icon {
|
||||
--tile-icon-color: var(--tile-color);
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
position: relative;
|
||||
padding: 6px;
|
||||
margin: -6px;
|
||||
}
|
||||
.icon-container ha-tile-badge {
|
||||
ha-tile-badge {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
inset-inline-end: -3px;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
inset-inline-end: 3px;
|
||||
inset-inline-start: initial;
|
||||
}
|
||||
.icon-container[role="button"] {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.icon-container[role="button"]:focus-visible,
|
||||
.icon-container[role="button"]:active {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
ha-tile-info {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
@@ -447,6 +455,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
hui-card-features {
|
||||
--feature-color: var(--tile-color);
|
||||
padding: 0 12px 12px 12px;
|
||||
}
|
||||
.container.horizontal hui-card-features {
|
||||
width: 50%;
|
||||
--feature-height: 36px;
|
||||
padding: 10px;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"],
|
||||
|
@@ -37,6 +37,7 @@ import type {
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import type { WeatherForecastCardConfig } from "./types";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
|
||||
@customElement("hui-weather-forecast-card")
|
||||
class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
@@ -106,7 +107,9 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
!this.isConnected ||
|
||||
!this.hass ||
|
||||
!this._config ||
|
||||
!this._needForecastSubscription()
|
||||
!this._needForecastSubscription() ||
|
||||
!isComponentLoaded(this.hass, "weather") ||
|
||||
!this.hass.states[this._config!.entity]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -118,7 +121,14 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
(event) => {
|
||||
this._forecastEvent = event;
|
||||
}
|
||||
);
|
||||
).catch((e) => {
|
||||
if (e.code === "invalid_entity_id") {
|
||||
setTimeout(() => {
|
||||
this._subscribed = undefined;
|
||||
}, 2000);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
|
@@ -533,6 +533,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
||||
icon_hold_action?: ActionConfig;
|
||||
icon_double_tap_action?: ActionConfig;
|
||||
features?: LovelaceCardFeatureConfig[];
|
||||
features_position?: "bottom" | "inline";
|
||||
}
|
||||
|
||||
export interface HeadingCardConfig extends LovelaceCardConfig {
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
startOfWeek,
|
||||
startOfYear,
|
||||
subDays,
|
||||
subMonths,
|
||||
} from "date-fns";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -179,6 +180,30 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
calcDate(today, startOfYear, this.hass.locale, this.hass.config),
|
||||
calcDate(today, endOfYear, this.hass.locale, this.hass.config),
|
||||
],
|
||||
[this.hass.localize("ui.components.date-range-picker.ranges.now-7d")]: [
|
||||
calcDate(today, subDays, this.hass.locale, this.hass.config, 7),
|
||||
calcDate(today, subDays, this.hass.locale, this.hass.config, 1),
|
||||
],
|
||||
[this.hass.localize("ui.components.date-range-picker.ranges.now-30d")]:
|
||||
[
|
||||
calcDate(today, subDays, this.hass.locale, this.hass.config, 30),
|
||||
calcDate(today, subDays, this.hass.locale, this.hass.config, 1),
|
||||
],
|
||||
[this.hass.localize("ui.components.date-range-picker.ranges.now-12m")]:
|
||||
[
|
||||
calcDate(
|
||||
subMonths(today, 12),
|
||||
startOfMonth,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
calcDate(
|
||||
subMonths(today, 1),
|
||||
endOfMonth,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -248,6 +273,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
.ranges=${this._ranges}
|
||||
@value-changed=${this._dateRangeChanged}
|
||||
minimal
|
||||
header-position
|
||||
></ha-date-range-picker>
|
||||
</div>
|
||||
|
||||
|
@@ -200,8 +200,6 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: 8px;
|
||||
flex: 1 1 30%;
|
||||
min-height: 40px;
|
||||
align-content: center;
|
||||
}
|
||||
.info,
|
||||
.info > * {
|
||||
@@ -235,8 +233,6 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
}
|
||||
.value {
|
||||
direction: ltr;
|
||||
min-height: 40px;
|
||||
align-content: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -93,6 +93,7 @@ export class HuiEntityBadgeEditor
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
default_color: "state",
|
||||
include_state: true,
|
||||
},
|
||||
},
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
enums,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
} from "superstruct";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
@@ -48,12 +50,25 @@ const cardConfigStruct = assign(
|
||||
show_entity_picture: optional(boolean()),
|
||||
vertical: optional(boolean()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
icon_tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
double_tap_action: optional(actionConfigStruct),
|
||||
icon_tap_action: optional(actionConfigStruct),
|
||||
icon_hold_action: optional(actionConfigStruct),
|
||||
icon_double_tap_action: optional(actionConfigStruct),
|
||||
features: optional(array(any())),
|
||||
features_position: optional(enums(["bottom", "inline"])),
|
||||
})
|
||||
);
|
||||
|
||||
const ADVANCED_ACTIONS = [
|
||||
"hold_action",
|
||||
"icon_hold_action",
|
||||
"double_tap_action",
|
||||
"icon_double_tap_action",
|
||||
] as const;
|
||||
|
||||
type AdvancedActions = (typeof ADVANCED_ACTIONS)[number];
|
||||
|
||||
@customElement("hui-tile-card-editor")
|
||||
export class HuiTileCardEditor
|
||||
extends LitElement
|
||||
@@ -63,13 +78,46 @@ export class HuiTileCardEditor
|
||||
|
||||
@state() private _config?: TileCardConfig;
|
||||
|
||||
@state() private _displayActions?: AdvancedActions[];
|
||||
|
||||
public setConfig(config: TileCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
|
||||
if (this._displayActions) return;
|
||||
this._setDisplayActions(config);
|
||||
}
|
||||
|
||||
private _setDisplayActions(config: TileCardConfig) {
|
||||
this._displayActions = ADVANCED_ACTIONS.filter(
|
||||
(action) => action in config
|
||||
);
|
||||
}
|
||||
|
||||
private _resetConfiguredActions() {
|
||||
this._displayActions = undefined;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this._config) {
|
||||
this._setDisplayActions(this._config);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._resetConfiguredActions();
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(entityId: string | undefined, hideState: boolean) =>
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
entityId: string | undefined,
|
||||
hideState: boolean,
|
||||
vertical: boolean,
|
||||
displayActions: AdvancedActions[] = []
|
||||
) =>
|
||||
[
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{
|
||||
@@ -105,12 +153,6 @@ export class HuiTileCardEditor
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "vertical",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hide_state",
|
||||
selector: {
|
||||
@@ -132,6 +174,43 @@ export class HuiTileCardEditor
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "content_layout",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["horizontal", "vertical"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.content_layout_options.${value}`
|
||||
),
|
||||
value,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "features_position",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["bottom", "inline"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.features_position_options.${value}`
|
||||
),
|
||||
value,
|
||||
disabled: vertical && value === "inline",
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -158,14 +237,14 @@ export class HuiTileCardEditor
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hold_action",
|
||||
...displayActions.map((action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
})),
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
@@ -179,9 +258,23 @@ export class HuiTileCardEditor
|
||||
const entityId = this._config!.entity;
|
||||
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
|
||||
|
||||
const schema = this._schema(entityId, this._config!.hide_state ?? false);
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
entityId,
|
||||
this._config.hide_state ?? false,
|
||||
this._config.vertical ?? false,
|
||||
this._displayActions
|
||||
);
|
||||
|
||||
const data = this._config;
|
||||
const data = {
|
||||
...this._config,
|
||||
content_layout: this._config.vertical ? "vertical" : "horizontal",
|
||||
};
|
||||
|
||||
// Default features position to bottom and force it to bottom in vertical mode
|
||||
if (!data.features_position || data.vertical) {
|
||||
data.features_position = "bottom";
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -233,6 +326,12 @@ export class HuiTileCardEditor
|
||||
delete config.state_content;
|
||||
}
|
||||
|
||||
// Convert content_layout to vertical
|
||||
if (config.content_layout) {
|
||||
config.vertical = config.content_layout === "vertical";
|
||||
delete config.content_layout;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
@@ -287,12 +386,14 @@ export class HuiTileCardEditor
|
||||
switch (schema.name) {
|
||||
case "color":
|
||||
case "icon_tap_action":
|
||||
case "icon_hold_action":
|
||||
case "icon_double_tap_action":
|
||||
case "show_entity_picture":
|
||||
case "vertical":
|
||||
case "hide_state":
|
||||
case "state_content":
|
||||
case "content_layout":
|
||||
case "appearance":
|
||||
case "interactions":
|
||||
case "features_position":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.tile.${schema.name}`
|
||||
);
|
||||
@@ -328,6 +429,14 @@ export class HuiTileCardEditor
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.info {
|
||||
color: var(--secondary-text-color);
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.features-form {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -147,12 +147,13 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
if (!this.lovelace) return nothing;
|
||||
|
||||
const sections = this.sections;
|
||||
const totalSectionCount =
|
||||
this._sectionColumnCount + (this.lovelace?.editMode ? 1 : 0);
|
||||
const editMode = this.lovelace.editMode;
|
||||
|
||||
const maxColumnCount = this._columnsController.value ?? 1;
|
||||
const totalSectionCount = this._sectionColumnCount;
|
||||
|
||||
const showExtraColumn =
|
||||
totalSectionCount < maxColumnCount && totalSectionCount > 0 && editMode;
|
||||
return html`
|
||||
<hui-view-badges
|
||||
.hass=${this.hass}
|
||||
@@ -174,6 +175,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
<div
|
||||
class="container ${classMap({
|
||||
dense: Boolean(this._config?.dense_section_placement),
|
||||
"extra-column": showExtraColumn,
|
||||
})}"
|
||||
style=${styleMap({
|
||||
"--total-section-count": totalSectionCount,
|
||||
@@ -250,13 +252,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
.rollback=${false}
|
||||
>
|
||||
<div class="create-section-container">
|
||||
<div class="drop-helper" aria-hidden="true">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.section.drop_card_create_section"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="create-section"
|
||||
@click=${this._createSection}
|
||||
@@ -456,6 +451,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
.container {
|
||||
--column-count: min(var(--max-column-count), var(--total-section-count));
|
||||
--container-max-width: calc(
|
||||
var(--column-count) * var(--column-max-width) +
|
||||
(var(--column-count) - 1) * var(--column-gap)
|
||||
);
|
||||
display: grid;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
@@ -465,11 +464,14 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
padding: var(--row-gap) var(--column-gap);
|
||||
box-sizing: content-box;
|
||||
margin: 0 auto;
|
||||
max-width: calc(
|
||||
var(--column-count) * var(--column-max-width) +
|
||||
(var(--column-count) - 1) * var(--column-gap)
|
||||
);
|
||||
max-width: var(--container-max-width);
|
||||
}
|
||||
|
||||
.container.extra-column {
|
||||
grid-template-columns: repeat(var(--column-count), 1fr) 76px;
|
||||
max-width: calc(var(--column-gap) + 76px + var(--container-max-width));
|
||||
}
|
||||
|
||||
.container.dense {
|
||||
grid-auto-flow: row dense;
|
||||
}
|
||||
@@ -483,38 +485,20 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 36px;
|
||||
margin-top: 34px;
|
||||
}
|
||||
|
||||
.create-section-container .card {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.create-section-container:has(.card) .drop-helper {
|
||||
display: flex;
|
||||
}
|
||||
.create-section-container:has(.card) .create-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.drop-helper {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
outline: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
border: 2px dashed var(--primary-color);
|
||||
height: calc(var(--row-height) + 2 * (var(--row-gap) + 2px));
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
--ha-ripple-color: var(--primary-color);
|
||||
--ha-ripple-hover-opacity: 0.04;
|
||||
--ha-ripple-pressed-opacity: 0.12;
|
||||
.create-section-container:has(.card) .create-section:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
inset: 0;
|
||||
background: var(--primary-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
|
||||
.drop-helper p {
|
||||
@@ -537,7 +521,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
height: calc(var(--row-height) + 2 * (var(--row-gap) + 2px));
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
width: 76px;
|
||||
--ha-ripple-color: var(--primary-color);
|
||||
--ha-ripple-hover-opacity: 0.04;
|
||||
--ha-ripple-pressed-opacity: 0.12;
|
||||
|
@@ -1,14 +1,11 @@
|
||||
import { mdiAlertCircleOutline } from "@mdi/js";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-formfield";
|
||||
import "../../components/ha-radio";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-list-item";
|
||||
import type { HaRadio } from "../../components/ha-radio";
|
||||
import "../../components/ha-select";
|
||||
import "../../components/ha-settings-row";
|
||||
@@ -17,10 +14,8 @@ import {
|
||||
DEFAULT_ACCENT_COLOR,
|
||||
DEFAULT_PRIMARY_COLOR,
|
||||
} from "../../resources/styles-data";
|
||||
import type { HomeAssistant, ThemeSettings } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import type { StorageLocation } from "../../state/themes-mixin";
|
||||
import { subscribeSelectedTheme } from "../../data/ws-themes";
|
||||
|
||||
const USE_DEFAULT_THEME = "__USE_DEFAULT_THEME__";
|
||||
const HOME_ASSISTANT_THEME = "default";
|
||||
@@ -31,78 +26,57 @@ export class HaPickThemeRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public storageLocation: StorageLocation = "browser";
|
||||
|
||||
@state() _themeNames: string[] = [];
|
||||
|
||||
@state() private _selectedTheme?: ThemeSettings;
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._loading) {
|
||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
const hasThemes =
|
||||
this.hass.themes.themes && Object.keys(this.hass.themes.themes).length;
|
||||
|
||||
const curThemeIsUseDefault = this._selectedTheme?.theme === "";
|
||||
const curTheme = this._selectedTheme?.theme
|
||||
? this._selectedTheme?.theme
|
||||
: this._selectedTheme?.dark
|
||||
const curThemeIsUseDefault = this.hass.selectedTheme?.theme === "";
|
||||
const curTheme = this.hass.selectedTheme?.theme
|
||||
? this.hass.selectedTheme?.theme
|
||||
: this.hass.themes.darkMode
|
||||
? this.hass.themes.default_dark_theme || this.hass.themes.default_theme
|
||||
: this.hass.themes.default_theme;
|
||||
|
||||
const themeSettings = this.hass.selectedTheme;
|
||||
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize("ui.panel.profile.themes.header")}</span
|
||||
>
|
||||
<span
|
||||
slot="description"
|
||||
class=${this.storageLocation === "user" &&
|
||||
this.hass.browserThemeEnabled
|
||||
? "device-info"
|
||||
: ""}
|
||||
>
|
||||
${!hasThemes &&
|
||||
!(this.storageLocation === "user" && this.hass.browserThemeEnabled)
|
||||
<span slot="description">
|
||||
${!hasThemes
|
||||
? this.hass.localize("ui.panel.profile.themes.error_no_theme")
|
||||
: ""}
|
||||
${this.storageLocation === "user" && this.hass.browserThemeEnabled
|
||||
? html`<ha-svg-icon .path=${mdiAlertCircleOutline}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.themes.device.user_theme_info"
|
||||
)}`
|
||||
: html`<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/integrations/frontend/#defining-themes"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.profile.themes.link_promo")}
|
||||
</a>`}
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/integrations/frontend/#defining-themes"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.profile.themes.link_promo")}
|
||||
</a>
|
||||
</span>
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.panel.profile.themes.dropdown_label")}
|
||||
.disabled=${!hasThemes}
|
||||
.value=${this._selectedTheme?.theme || USE_DEFAULT_THEME}
|
||||
.value=${this.hass.selectedTheme?.theme || USE_DEFAULT_THEME}
|
||||
@selected=${this._handleThemeSelection}
|
||||
naturalMenuWidth
|
||||
>
|
||||
<ha-list-item .value=${USE_DEFAULT_THEME}>
|
||||
<mwc-list-item .value=${USE_DEFAULT_THEME}>
|
||||
${this.hass.localize("ui.panel.profile.themes.use_default")}
|
||||
</ha-list-item>
|
||||
<ha-list-item .value=${HOME_ASSISTANT_THEME}>
|
||||
</mwc-list-item>
|
||||
<mwc-list-item .value=${HOME_ASSISTANT_THEME}>
|
||||
Home Assistant
|
||||
</ha-list-item>
|
||||
</mwc-list-item>
|
||||
${this._themeNames.map(
|
||||
(theme) => html`
|
||||
<ha-list-item .value=${theme}>${theme}</ha-list-item>
|
||||
<mwc-list-item .value=${theme}>${theme}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
@@ -112,7 +86,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
this.hass.themes.default_dark_theme &&
|
||||
this.hass.themes.default_theme) ||
|
||||
this._supportsModeSelection(curTheme)
|
||||
? html`<div class="inputs">
|
||||
? html` <div class="inputs">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.auto"
|
||||
@@ -122,7 +96,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="auto"
|
||||
.checked=${this._selectedTheme?.dark === undefined}
|
||||
.checked=${themeSettings?.dark === undefined}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
@@ -134,7 +108,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="light"
|
||||
.checked=${this._selectedTheme?.dark === false}
|
||||
.checked=${themeSettings?.dark === false}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
@@ -147,14 +121,14 @@ export class HaPickThemeRow extends LitElement {
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="dark"
|
||||
.checked=${this._selectedTheme?.dark === true}
|
||||
.checked=${themeSettings?.dark === true}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
${curTheme === HOME_ASSISTANT_THEME
|
||||
? html`<div class="color-pickers">
|
||||
<ha-textfield
|
||||
.value=${this._selectedTheme?.primaryColor ||
|
||||
.value=${themeSettings?.primaryColor ||
|
||||
DEFAULT_PRIMARY_COLOR}
|
||||
type="color"
|
||||
.label=${this.hass.localize(
|
||||
@@ -164,8 +138,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
@change=${this._handleColorChange}
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
.value=${this._selectedTheme?.accentColor ||
|
||||
DEFAULT_ACCENT_COLOR}
|
||||
.value=${themeSettings?.accentColor || DEFAULT_ACCENT_COLOR}
|
||||
type="color"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.accent_color"
|
||||
@@ -173,36 +146,19 @@ export class HaPickThemeRow extends LitElement {
|
||||
.name=${"accentColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></ha-textfield>
|
||||
${this._selectedTheme?.primaryColor ||
|
||||
this._selectedTheme?.accentColor
|
||||
? html`<ha-button @click=${this._resetColors}>
|
||||
${themeSettings?.primaryColor || themeSettings?.accentColor
|
||||
? html` <mwc-button @click=${this._resetColors}>
|
||||
${this.hass.localize("ui.panel.profile.themes.reset")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</div>`
|
||||
: nothing}
|
||||
: ""}
|
||||
</div>`
|
||||
: nothing}
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
if (this.storageLocation === "browser") {
|
||||
this._selectedTheme = this.hass.selectedTheme ?? undefined;
|
||||
} else {
|
||||
this._loading = true;
|
||||
this._selectedTheme = undefined;
|
||||
subscribeSelectedTheme(
|
||||
this.hass,
|
||||
(selectedTheme?: ThemeSettings | null) => {
|
||||
this._selectedTheme = selectedTheme ?? undefined;
|
||||
this._loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const oldHass = changedProperties.get("hass") as undefined | HomeAssistant;
|
||||
const themesChanged =
|
||||
changedProperties.has("hass") &&
|
||||
@@ -213,34 +169,13 @@ export class HaPickThemeRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _shouldSaveHass() {
|
||||
return (
|
||||
this.storageLocation === "browser" ||
|
||||
(this.storageLocation === "user" && !this.hass.browserThemeEnabled)
|
||||
);
|
||||
}
|
||||
|
||||
private _updateSelectedTheme(updatedTheme: Partial<ThemeSettings>) {
|
||||
this._selectedTheme = {
|
||||
...this._selectedTheme,
|
||||
...updatedTheme,
|
||||
theme: updatedTheme.theme ?? this._selectedTheme?.theme ?? "",
|
||||
};
|
||||
|
||||
fireEvent(this, "settheme", {
|
||||
settings: this._selectedTheme,
|
||||
storageLocation: this.storageLocation,
|
||||
saveHass: this._shouldSaveHass(),
|
||||
});
|
||||
}
|
||||
|
||||
private _handleColorChange(ev: CustomEvent) {
|
||||
const target = ev.target as any;
|
||||
this._updateSelectedTheme({ [target.name]: target.value });
|
||||
fireEvent(this, "settheme", { [target.name]: target.value });
|
||||
}
|
||||
|
||||
private _resetColors() {
|
||||
this._updateSelectedTheme({
|
||||
fireEvent(this, "settheme", {
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
});
|
||||
@@ -263,18 +198,18 @@ export class HaPickThemeRow extends LitElement {
|
||||
dark = true;
|
||||
break;
|
||||
}
|
||||
this._updateSelectedTheme({ dark });
|
||||
fireEvent(this, "settheme", { dark });
|
||||
}
|
||||
|
||||
private _handleThemeSelection(ev) {
|
||||
const theme = ev.target.value;
|
||||
if (theme === this._selectedTheme?.theme) {
|
||||
if (theme === this.hass.selectedTheme?.theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme === USE_DEFAULT_THEME) {
|
||||
if (this.hass.selectedTheme?.theme) {
|
||||
this._updateSelectedTheme({
|
||||
fireEvent(this, "settheme", {
|
||||
theme: "",
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
@@ -282,7 +217,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._updateSelectedTheme({
|
||||
fireEvent(this, "settheme", {
|
||||
theme,
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
@@ -314,12 +249,6 @@ export class HaPickThemeRow extends LitElement {
|
||||
flex-grow: 1;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.device-info {
|
||||
color: var(--warning-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -1,12 +1,10 @@
|
||||
import "@material/mwc-button";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-expansion-panel";
|
||||
import "../../components/ha-switch";
|
||||
import "../../layouts/hass-tabs-subpage";
|
||||
import { profileSections } from "./ha-panel-profile";
|
||||
import { isExternal } from "../../data/external";
|
||||
@@ -29,7 +27,6 @@ import "./ha-pick-time-zone-row";
|
||||
import "./ha-push-notifications-row";
|
||||
import "./ha-set-suspend-row";
|
||||
import "./ha-set-vibrate-row";
|
||||
import type { HaSwitch } from "../../components/ha-switch";
|
||||
|
||||
@customElement("ha-profile-section-general")
|
||||
class HaProfileSectionGeneral extends LitElement {
|
||||
@@ -41,8 +38,6 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _browserThemeActivated = false;
|
||||
|
||||
private _unsubCoreData?: UnsubscribeFunc;
|
||||
|
||||
private _getCoreData() {
|
||||
@@ -96,9 +91,9 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button class="warning" @click=${this._handleLogOut}>
|
||||
<mwc-button class="warning" @click=${this._handleLogOut}>
|
||||
${this.hass.localize("ui.panel.profile.logout")}
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
@@ -133,12 +128,6 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-first-weekday-row>
|
||||
<ha-pick-theme-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
this._browserThemeActivated}
|
||||
.storageLocation=${"user"}
|
||||
></ha-pick-theme-row>
|
||||
${this.hass.user!.is_admin
|
||||
? html`
|
||||
<ha-advanced-mode-row
|
||||
@@ -159,42 +148,10 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.client_settings_detail")}
|
||||
</div>
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
isExternal
|
||||
? "ui.panel.profile.themes.device.mobile_app_header"
|
||||
: "ui.panel.profile.themes.device.browser_header"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.themes.device.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.checked=${this.hass.browserThemeEnabled ||
|
||||
this._browserThemeActivated}
|
||||
@change=${this._toggleBrowserTheme}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
${this.hass.browserThemeEnabled || this._browserThemeActivated
|
||||
? html`
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.profile.themes.device.custom_theme"
|
||||
)}
|
||||
expanded
|
||||
>
|
||||
<ha-pick-theme-row
|
||||
class="device-theme"
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-theme-row>
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: nothing}
|
||||
<ha-pick-theme-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-theme-row>
|
||||
<ha-pick-dashboard-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
@@ -210,11 +167,11 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
"ui.panel.profile.customize_sidebar.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button @click=${this._customizeSidebar}>
|
||||
<mwc-button @click=${this._customizeSidebar}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.button"
|
||||
)}
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
</ha-settings-row>
|
||||
${this.hass.dockedSidebar !== "auto" || !this.narrow
|
||||
? html`
|
||||
@@ -268,39 +225,6 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _toggleBrowserTheme(ev: Event) {
|
||||
const switchElement = ev.target as HaSwitch;
|
||||
const enabled = switchElement.checked;
|
||||
|
||||
if (!enabled) {
|
||||
if (!this.hass.browserThemeEnabled && this._browserThemeActivated) {
|
||||
// no changed have made, disable without confirmation
|
||||
this._browserThemeActivated = false;
|
||||
} else {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.profile.themes.device.delete_header"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.profile.themes.device.delete_description"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (confirm) {
|
||||
this._browserThemeActivated = false;
|
||||
fireEvent(this, "resetBrowserTheme");
|
||||
} else {
|
||||
// revert switch
|
||||
switchElement.click();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._browserThemeActivated = true;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -327,14 +251,6 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
margin: 0 8px 8px;
|
||||
}
|
||||
.device-theme {
|
||||
display: block;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -3,37 +3,18 @@ import {
|
||||
invalidateThemeCache,
|
||||
} from "../common/dom/apply_themes_on_element";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
fetchSelectedTheme,
|
||||
saveSelectedTheme,
|
||||
SELECTED_THEME_KEY,
|
||||
subscribeSelectedTheme,
|
||||
subscribeThemes,
|
||||
} from "../data/ws-themes";
|
||||
import type { Constructor, HomeAssistant, ThemeSettings } from "../types";
|
||||
import { clearStateKey, storeState } from "../util/ha-pref-storage";
|
||||
import { subscribeThemes } from "../data/ws-themes";
|
||||
import type { Constructor, HomeAssistant } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import type { HassBaseEl } from "./hass-base-mixin";
|
||||
|
||||
export type StorageLocation = "user" | "browser";
|
||||
|
||||
interface SetThemeSettings {
|
||||
settings: ThemeSettings;
|
||||
storageLocation: StorageLocation;
|
||||
saveHass: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
settheme: HASSDomEvent<SetThemeSettings>;
|
||||
settheme: HASSDomEvent<Partial<HomeAssistant["selectedTheme"]>>;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
settheme: SetThemeSettings;
|
||||
resetBrowserTheme: undefined;
|
||||
}
|
||||
|
||||
interface FrontendUserData {
|
||||
selectedTheme?: ThemeSettings;
|
||||
settheme: Partial<HomeAssistant["selectedTheme"]>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,38 +27,16 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("settheme", (ev) => {
|
||||
if (ev.detail.saveHass) {
|
||||
this._updateHass({
|
||||
selectedTheme: ev.detail.settings,
|
||||
browserThemeEnabled: ev.detail.storageLocation === "browser",
|
||||
});
|
||||
this._animateApplyTheme(mql.matches);
|
||||
}
|
||||
|
||||
if (ev.detail.storageLocation === "browser") {
|
||||
storeState(this.hass!);
|
||||
} else {
|
||||
if (ev.detail.saveHass) {
|
||||
clearStateKey(SELECTED_THEME_KEY);
|
||||
}
|
||||
saveSelectedTheme(this.hass!, ev.detail.settings);
|
||||
}
|
||||
});
|
||||
|
||||
this.addEventListener("resetBrowserTheme", async () => {
|
||||
clearStateKey(SELECTED_THEME_KEY);
|
||||
const selectedTheme = await fetchSelectedTheme(this.hass!);
|
||||
this._updateHass({
|
||||
selectedTheme,
|
||||
browserThemeEnabled: false,
|
||||
selectedTheme: {
|
||||
...this.hass!.selectedTheme!,
|
||||
...ev.detail,
|
||||
},
|
||||
});
|
||||
this._animateApplyTheme(mql.matches);
|
||||
this._applyTheme(mql.matches);
|
||||
storeState(this.hass!);
|
||||
});
|
||||
|
||||
mql.addEventListener("change", (ev) =>
|
||||
this._animateApplyTheme(ev.matches)
|
||||
);
|
||||
|
||||
mql.addListener((ev) => this._applyTheme(ev.matches));
|
||||
if (!this._themeApplied && mql.matches) {
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
@@ -104,62 +63,6 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
invalidateThemeCache();
|
||||
this._applyTheme(mql.matches);
|
||||
});
|
||||
|
||||
subscribeSelectedTheme(
|
||||
this.hass!,
|
||||
(selectedTheme?: ThemeSettings | null) => {
|
||||
if (
|
||||
!window.localStorage.getItem(SELECTED_THEME_KEY) &&
|
||||
selectedTheme
|
||||
) {
|
||||
this._themeApplied = true;
|
||||
this._updateHass({
|
||||
selectedTheme,
|
||||
browserThemeEnabled: false,
|
||||
});
|
||||
this._applyTheme(mql.matches);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _animateApplyTheme(darkPreferred: boolean) {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!document.startViewTransition || !document.documentElement.animate) {
|
||||
this._applyTheme(darkPreferred);
|
||||
} else {
|
||||
await document.startViewTransition(() => {
|
||||
this._applyTheme(darkPreferred);
|
||||
}).ready;
|
||||
|
||||
const { top, left, width, height } =
|
||||
document.documentElement.getBoundingClientRect();
|
||||
const x = left + width / 2;
|
||||
const y = top + height / 2;
|
||||
const right = window.innerWidth - left;
|
||||
const bottom = window.innerHeight - top;
|
||||
const maxRadius = Math.hypot(
|
||||
Math.max(left, right),
|
||||
Math.max(top, bottom)
|
||||
);
|
||||
|
||||
document.documentElement.animate(
|
||||
{
|
||||
clipPath: [
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${maxRadius}px at ${x}px ${y}px)`,
|
||||
],
|
||||
},
|
||||
{
|
||||
duration: 500,
|
||||
easing: "linear(0, 0.1, 1)",
|
||||
pseudoElement: "::view-transition-new(root)",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _applyTheme(darkPreferred: boolean) {
|
||||
|
@@ -228,7 +228,7 @@
|
||||
"run": "[%key:ui::card::service::run%]",
|
||||
"running_single": "Running…",
|
||||
"running_queued": "{queued} queued",
|
||||
"running_parallel": "{active} Running…",
|
||||
"running_parallel": "{active} running…",
|
||||
"cancel": "Cancel",
|
||||
"cancel_multiple": "Cancel {number}",
|
||||
"cancel_all": "Cancel all",
|
||||
@@ -809,10 +809,16 @@
|
||||
"ranges": {
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"now-1h": "Last hour",
|
||||
"now-12h": "Last 12 hours",
|
||||
"now-24h": "Last 24 hours",
|
||||
"this_week": "This week",
|
||||
"this_quarter": "This quarter",
|
||||
"this_month": "This month",
|
||||
"this_year": "This year"
|
||||
"now-7d": "Last 7 days",
|
||||
"now-30d": "Last 30 days",
|
||||
"this_year": "This year",
|
||||
"now-12m": "Last 12 month"
|
||||
}
|
||||
},
|
||||
"grid-size-picker": {
|
||||
@@ -2463,7 +2469,7 @@
|
||||
},
|
||||
"retention": "Retention",
|
||||
"custom_retention": "Custom retention",
|
||||
"custom_retention_label": "Clean up every",
|
||||
"custom_retention_label": "Keep only",
|
||||
"retention_description": "Based on the maximum number of backups or how many days they should be kept.",
|
||||
"retention_presets": {
|
||||
"copies_3": "3 backups",
|
||||
@@ -2506,6 +2512,7 @@
|
||||
"menu": {
|
||||
"upload_backup": "Upload backup"
|
||||
},
|
||||
"agent_error": "Error in location {name}",
|
||||
"new_backup": "Backup now",
|
||||
"onboarding": {
|
||||
"title": "Set up backups",
|
||||
@@ -2556,7 +2563,7 @@
|
||||
"last_backup_failed_heading": "Last automatic backup failed",
|
||||
"last_backup_failed_description": "The last automatic backup triggered {relative_time} wasn't successful.",
|
||||
"last_backup_failed_locations_description": "The last automatic backup created {relative_time} wasn't stored in all locations.",
|
||||
"last_successful_backup_description": "Last successful backup {relative_time} and stored in {count} {count, plural,\n one {location}\n other {locations}\n}.",
|
||||
"last_successful_backup_description": "Last successful automatic backup {relative_time} and stored in {count} {count, plural,\n one {location}\n other {locations}\n}.",
|
||||
"no_backup_heading": "No automatic backup available",
|
||||
"no_backup_description": "You have no automatic backups yet.",
|
||||
"backup_too_old_heading": "No backup for {count} {count, plural,\n one {day}\n other {days}\n}",
|
||||
@@ -2644,7 +2651,9 @@
|
||||
"title": "Locations",
|
||||
"description": "Your backup will be stored on these locations when this default backup is created. You can use all locations for custom backups.",
|
||||
"no_location": "No location selected",
|
||||
"no_location_description": "You have to select at least one location to create a backup."
|
||||
"no_location_description": "You have to select at least one location to create a backup.",
|
||||
"more_locations": "Explore more locations",
|
||||
"manage_network_storage": "Manage network storage"
|
||||
},
|
||||
"encryption_key": {
|
||||
"title": "Encryption key",
|
||||
@@ -6301,7 +6310,8 @@
|
||||
"description": "Home Assistant is starting, please wait…"
|
||||
},
|
||||
"map": {
|
||||
"reset_focus": "Reset focus"
|
||||
"reset_focus": "Reset focus",
|
||||
"toggle_grouping": "Toggle grouping"
|
||||
},
|
||||
"energy": {
|
||||
"loading": "Loading…",
|
||||
@@ -7111,12 +7121,22 @@
|
||||
"color": "Color",
|
||||
"color_helper": "Inactive state (e.g. off, closed) will not be colored.",
|
||||
"icon_tap_action": "Icon tap behavior",
|
||||
"interactions": "Interactions",
|
||||
"icon_hold_action": "Icon hold behavior",
|
||||
"icon_double_tap_action": "Icon double tap behavior",
|
||||
"appearance": "Appearance",
|
||||
"show_entity_picture": "Show entity picture",
|
||||
"vertical": "Vertical",
|
||||
"hide_state": "Hide state",
|
||||
"state_content": "State content"
|
||||
"state_content": "State content",
|
||||
"features_position": "Features position",
|
||||
"features_position_options": {
|
||||
"bottom": "Bottom",
|
||||
"inline": "Inline"
|
||||
},
|
||||
"content_layout": "Content layout",
|
||||
"content_layout_options": {
|
||||
"horizontal": "Horizontal",
|
||||
"vertical": "Vertical"
|
||||
}
|
||||
},
|
||||
"vertical-stack": {
|
||||
"name": "Vertical stack",
|
||||
@@ -7575,16 +7595,7 @@
|
||||
"primary_color": "Primary color",
|
||||
"accent_color": "Accent color",
|
||||
"reset": "Reset",
|
||||
"use_default": "Use default theme",
|
||||
"device": {
|
||||
"browser_header": "Browser theme",
|
||||
"mobile_app_header": "Mobile app theme",
|
||||
"custom_theme": "Custom theme",
|
||||
"description": "Overwrite user theme with custom device settings",
|
||||
"delete_header": "Delete device theme",
|
||||
"delete_description": "Are you sure you want to delete the device specific theme?",
|
||||
"user_theme_info": "Device theme is active. You won't see changes on user theme settings."
|
||||
}
|
||||
"use_default": "Use default theme"
|
||||
},
|
||||
"dashboard": {
|
||||
"header": "Dashboard",
|
||||
|
@@ -223,7 +223,6 @@ export interface HomeAssistant {
|
||||
config: HassConfig;
|
||||
themes: Themes;
|
||||
selectedTheme: ThemeSettings | null;
|
||||
browserThemeEnabled?: boolean;
|
||||
panels: Panels;
|
||||
panelUrl: string;
|
||||
// i18n
|
||||
|
24
src/util/cache-manager.ts
Normal file
24
src/util/cache-manager.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export class CacheManager<T> {
|
||||
constructor(expiration?: number) {
|
||||
this._expiration = expiration;
|
||||
}
|
||||
|
||||
private _expiration?: number;
|
||||
|
||||
private _cache = new Map<string, T>();
|
||||
|
||||
public get(key: string): T | undefined {
|
||||
return this._cache.get(key);
|
||||
}
|
||||
|
||||
public set(key: string, value: T): void {
|
||||
this._cache.set(key, value);
|
||||
if (this._expiration) {
|
||||
window.setTimeout(() => this._cache.delete(key), this._expiration);
|
||||
}
|
||||
}
|
||||
|
||||
public has(key: string): boolean {
|
||||
return this._cache.has(key);
|
||||
}
|
||||
}
|
@@ -1,9 +1,8 @@
|
||||
import { SELECTED_THEME_KEY } from "../data/ws-themes";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
const STORED_STATE = [
|
||||
"dockedSidebar",
|
||||
SELECTED_THEME_KEY,
|
||||
"selectedTheme",
|
||||
"selectedLanguage",
|
||||
"vibrate",
|
||||
"debugConnection",
|
||||
@@ -12,17 +11,9 @@ const STORED_STATE = [
|
||||
"defaultPanel",
|
||||
];
|
||||
|
||||
const CLEARABLE_STATE = [SELECTED_THEME_KEY];
|
||||
|
||||
export function storeState(hass: HomeAssistant) {
|
||||
try {
|
||||
const states = [...STORED_STATE];
|
||||
|
||||
if (!hass.browserThemeEnabled) {
|
||||
states.splice(states.indexOf(SELECTED_THEME_KEY), 1);
|
||||
}
|
||||
|
||||
states.forEach((key) => {
|
||||
STORED_STATE.forEach((key) => {
|
||||
const value = hass[key];
|
||||
window.localStorage.setItem(
|
||||
key,
|
||||
@@ -41,18 +32,15 @@ export function storeState(hass: HomeAssistant) {
|
||||
}
|
||||
|
||||
export function getState() {
|
||||
const state: Partial<HomeAssistant> = {};
|
||||
const state = {};
|
||||
|
||||
STORED_STATE.forEach((key) => {
|
||||
const storageItem = window.localStorage.getItem(key);
|
||||
if (storageItem !== null) {
|
||||
let value = JSON.parse(storageItem);
|
||||
// selectedTheme went from string to object on 20200718
|
||||
if (key === SELECTED_THEME_KEY) {
|
||||
if (typeof value === "string") {
|
||||
value = { theme: value };
|
||||
}
|
||||
state.browserThemeEnabled = true;
|
||||
if (key === "selectedTheme" && typeof value === "string") {
|
||||
value = { theme: value };
|
||||
}
|
||||
// dockedSidebar went from boolean to enum on 20190720
|
||||
if (key === "dockedSidebar" && typeof value === "boolean") {
|
||||
@@ -67,9 +55,3 @@ export function getState() {
|
||||
export function clearState() {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
|
||||
export function clearStateKey(key: string) {
|
||||
if (CLEARABLE_STATE.includes(key)) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": 0
|
||||
}
|
||||
}
|
@@ -51,4 +51,16 @@ describe("Color Conversion Tests", () => {
|
||||
expect(theme2hex("#ff0000")).toBe("#ff0000");
|
||||
expect(theme2hex("unicorn")).toBe("unicorn");
|
||||
});
|
||||
|
||||
it("should convert rgb theme color to hex", () => {
|
||||
expect(theme2hex("rgb( 255, 0, 0)")).toBe("#ff0000");
|
||||
expect(theme2hex("rgb(0,255, 0)")).toBe("#00ff00");
|
||||
expect(theme2hex("rgb(0, 0,255 )")).toBe("#0000ff");
|
||||
});
|
||||
|
||||
it("should convert rgba theme color to hex by ignoring alpha", () => {
|
||||
expect(theme2hex("rgba( 255, 0, 0, 0.5)")).toBe("#ff0000");
|
||||
expect(theme2hex("rgba(0,255, 0, 0.3)")).toBe("#00ff00");
|
||||
expect(theme2hex("rgba(0, 0,255 , 0.7)")).toBe("#0000ff");
|
||||
});
|
||||
});
|
||||
|
45
test/common/entity/battery_icon.test.ts
Normal file
45
test/common/entity/battery_icon.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
batteryIcon,
|
||||
batteryLevelIcon,
|
||||
} from "../../../src/common/entity/battery_icon";
|
||||
|
||||
describe("batteryIcon", () => {
|
||||
it("should return correct icon for battery level", () => {
|
||||
const stateObj: HassEntity = { state: "50" } as HassEntity;
|
||||
expect(batteryIcon(stateObj)).toBe("mdi:battery-50");
|
||||
});
|
||||
|
||||
it("should return correct icon for battery level with state", () => {
|
||||
const stateObj: HassEntity = { state: "50" } as HassEntity;
|
||||
expect(batteryIcon(stateObj, "20")).toBe("mdi:battery-20");
|
||||
});
|
||||
});
|
||||
|
||||
describe("batteryLevelIcon", () => {
|
||||
it("should return correct icon for battery level", () => {
|
||||
expect(batteryLevelIcon(50)).toBe("mdi:battery-50");
|
||||
});
|
||||
|
||||
it("should return correct icon for charging battery", () => {
|
||||
expect(batteryLevelIcon(50, true)).toBe("mdi:battery-charging-50");
|
||||
});
|
||||
|
||||
it("should return charging outline icon for charging battery with 9%", () => {
|
||||
expect(batteryLevelIcon(9, true)).toBe("mdi:battery-charging-outline");
|
||||
});
|
||||
|
||||
it("should return alert icon for low battery", () => {
|
||||
expect(batteryLevelIcon(5)).toBe("mdi:battery-alert-variant-outline");
|
||||
});
|
||||
|
||||
it("should return unknown icon for invalid battery level", () => {
|
||||
expect(batteryLevelIcon("invalid")).toBe("mdi:battery-unknown");
|
||||
});
|
||||
|
||||
it("should return battery icon for on/off", () => {
|
||||
expect(batteryLevelIcon("off")).toBe("mdi:battery");
|
||||
expect(batteryLevelIcon("on")).toBe("mdi:battery-alert");
|
||||
});
|
||||
});
|
@@ -1,6 +1,7 @@
|
||||
import { assert, describe, it } from "vitest";
|
||||
|
||||
import { canToggleDomain } from "../../../src/common/entity/can_toggle_domain";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
|
||||
describe("canToggleDomain", () => {
|
||||
const hass: any = {
|
||||
@@ -9,10 +10,6 @@ describe("canToggleDomain", () => {
|
||||
turn_on: null, // Service keys only need to be present for test
|
||||
turn_off: null,
|
||||
},
|
||||
lock: {
|
||||
lock: null,
|
||||
unlock: null,
|
||||
},
|
||||
sensor: {
|
||||
custom_service: null,
|
||||
},
|
||||
@@ -23,10 +20,6 @@ describe("canToggleDomain", () => {
|
||||
assert.isTrue(canToggleDomain(hass, "light"));
|
||||
});
|
||||
|
||||
it("Detects locks toggle", () => {
|
||||
assert.isTrue(canToggleDomain(hass, "lock"));
|
||||
});
|
||||
|
||||
it("Detects sensors do not toggle", () => {
|
||||
assert.isFalse(canToggleDomain(hass, "sensor"));
|
||||
});
|
||||
@@ -34,4 +27,58 @@ describe("canToggleDomain", () => {
|
||||
it("Detects binary sensors do not toggle", () => {
|
||||
assert.isFalse(canToggleDomain(hass, "binary_sensor"));
|
||||
});
|
||||
|
||||
it("Detects covers toggle", () => {
|
||||
assert.isTrue(
|
||||
canToggleDomain(
|
||||
{
|
||||
services: {
|
||||
cover: {
|
||||
open_cover: null,
|
||||
},
|
||||
},
|
||||
} as unknown as HomeAssistant,
|
||||
"cover"
|
||||
)
|
||||
);
|
||||
assert.isFalse(
|
||||
canToggleDomain(
|
||||
{
|
||||
services: {
|
||||
cover: {
|
||||
open: null,
|
||||
},
|
||||
},
|
||||
} as unknown as HomeAssistant,
|
||||
"cover"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it("Detects lock toggle", () => {
|
||||
assert.isTrue(
|
||||
canToggleDomain(
|
||||
{
|
||||
services: {
|
||||
lock: {
|
||||
lock: null,
|
||||
},
|
||||
},
|
||||
} as unknown as HomeAssistant,
|
||||
"lock"
|
||||
)
|
||||
);
|
||||
assert.isFalse(
|
||||
canToggleDomain(
|
||||
{
|
||||
services: {
|
||||
lock: {
|
||||
unlock: null,
|
||||
},
|
||||
},
|
||||
} as unknown as HomeAssistant,
|
||||
"lock"
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
31
test/common/entity/color/battery_color.test.ts
Normal file
31
test/common/entity/color/battery_color.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { batteryStateColorProperty } from "../../../../src/common/entity/color/battery_color";
|
||||
|
||||
describe("battery_color", () => {
|
||||
it("should return green for high battery level", () => {
|
||||
let color = batteryStateColorProperty("70");
|
||||
expect(color).toBe("--state-sensor-battery-high-color");
|
||||
color = batteryStateColorProperty("200");
|
||||
expect(color).toBe("--state-sensor-battery-high-color");
|
||||
});
|
||||
|
||||
it("should return yellow for medium battery level", () => {
|
||||
let color = batteryStateColorProperty("69.99");
|
||||
expect(color).toBe("--state-sensor-battery-medium-color");
|
||||
color = batteryStateColorProperty("30");
|
||||
expect(color).toBe("--state-sensor-battery-medium-color");
|
||||
});
|
||||
|
||||
it("should return red for low battery level", () => {
|
||||
let color = batteryStateColorProperty("29.999");
|
||||
expect(color).toBe("--state-sensor-battery-low-color");
|
||||
color = batteryStateColorProperty("-20");
|
||||
expect(color).toBe("--state-sensor-battery-low-color");
|
||||
});
|
||||
|
||||
// add nan test
|
||||
it("should return undefined for non-numeric state", () => {
|
||||
const color = batteryStateColorProperty("not a number");
|
||||
expect(color).toBe(undefined);
|
||||
});
|
||||
});
|
56
test/common/entity/compute_state_name.test.ts
Normal file
56
test/common/entity/compute_state_name.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
computeStateName,
|
||||
computeStateNameFromEntityAttributes,
|
||||
} from "../../../src/common/entity/compute_state_name";
|
||||
|
||||
describe("computeStateName", () => {
|
||||
it("should return friendly_name if it exists", () => {
|
||||
const stateObj = {
|
||||
entity_id: "light.living_room",
|
||||
attributes: { friendly_name: "Living Room Light" },
|
||||
} as HassEntity;
|
||||
expect(computeStateName(stateObj)).toBe("Living Room Light");
|
||||
});
|
||||
|
||||
it("should return object id if friendly_name does not exist", () => {
|
||||
const stateObj = {
|
||||
entity_id: "light.living_room",
|
||||
attributes: {},
|
||||
} as HassEntity;
|
||||
expect(computeStateName(stateObj)).toBe("living room");
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeStateNameFromEntityAttributes", () => {
|
||||
it("should return friendly_name if it exists", () => {
|
||||
const entityId = "light.living_room";
|
||||
const attributes = { friendly_name: "Living Room Light" };
|
||||
expect(computeStateNameFromEntityAttributes(entityId, attributes)).toBe(
|
||||
"Living Room Light"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return friendly_name 0", () => {
|
||||
const entityId = "light.living_room";
|
||||
const attributes = { friendly_name: 0 };
|
||||
expect(computeStateNameFromEntityAttributes(entityId, attributes)).toBe(
|
||||
"0"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return empty if friendly_name is null", () => {
|
||||
const entityId = "light.living_room";
|
||||
const attributes = { friendly_name: null };
|
||||
expect(computeStateNameFromEntityAttributes(entityId, attributes)).toBe("");
|
||||
});
|
||||
|
||||
it("should return object id if friendly_name does not exist", () => {
|
||||
const entityId = "light.living_room";
|
||||
const attributes = {};
|
||||
expect(computeStateNameFromEntityAttributes(entityId, attributes)).toBe(
|
||||
"living room"
|
||||
);
|
||||
});
|
||||
});
|
54
test/common/entity/cover_icon.test.ts
Normal file
54
test/common/entity/cover_icon.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
mdiArrowCollapseHorizontal,
|
||||
mdiArrowDown,
|
||||
mdiArrowExpandHorizontal,
|
||||
mdiArrowUp,
|
||||
} from "@mdi/js";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
computeOpenIcon,
|
||||
computeCloseIcon,
|
||||
} from "../../../src/common/entity/cover_icon";
|
||||
|
||||
describe("computeOpenIcon", () => {
|
||||
it("returns mdiArrowExpandHorizontal for awning, door, gate, and curtain", () => {
|
||||
const stateObj = { attributes: { device_class: "awning" } } as HassEntity;
|
||||
expect(computeOpenIcon(stateObj)).toBe(mdiArrowExpandHorizontal);
|
||||
|
||||
stateObj.attributes.device_class = "door";
|
||||
expect(computeOpenIcon(stateObj)).toBe(mdiArrowExpandHorizontal);
|
||||
|
||||
stateObj.attributes.device_class = "gate";
|
||||
expect(computeOpenIcon(stateObj)).toBe(mdiArrowExpandHorizontal);
|
||||
|
||||
stateObj.attributes.device_class = "curtain";
|
||||
expect(computeOpenIcon(stateObj)).toBe(mdiArrowExpandHorizontal);
|
||||
});
|
||||
|
||||
it("returns mdiArrowUp for other device classes", () => {
|
||||
const stateObj = { attributes: { device_class: "window" } } as HassEntity;
|
||||
expect(computeOpenIcon(stateObj)).toBe(mdiArrowUp);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeCloseIcon", () => {
|
||||
it("returns mdiArrowCollapseHorizontal for awning, door, gate, and curtain", () => {
|
||||
const stateObj = { attributes: { device_class: "awning" } } as HassEntity;
|
||||
expect(computeCloseIcon(stateObj)).toBe(mdiArrowCollapseHorizontal);
|
||||
|
||||
stateObj.attributes.device_class = "door";
|
||||
expect(computeCloseIcon(stateObj)).toBe(mdiArrowCollapseHorizontal);
|
||||
|
||||
stateObj.attributes.device_class = "gate";
|
||||
expect(computeCloseIcon(stateObj)).toBe(mdiArrowCollapseHorizontal);
|
||||
|
||||
stateObj.attributes.device_class = "curtain";
|
||||
expect(computeCloseIcon(stateObj)).toBe(mdiArrowCollapseHorizontal);
|
||||
});
|
||||
|
||||
it("returns mdiArrowDown for other device classes", () => {
|
||||
const stateObj = { attributes: { device_class: "window" } } as HassEntity;
|
||||
expect(computeCloseIcon(stateObj)).toBe(mdiArrowDown);
|
||||
});
|
||||
});
|
194
test/common/entity/delete_entity.test.ts
Normal file
194
test/common/entity/delete_entity.test.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import {
|
||||
isDeletableEntity,
|
||||
deleteEntity,
|
||||
} from "../../../src/common/entity/delete_entity";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import type { EntityRegistryEntry } from "../../../src/data/entity_registry";
|
||||
import type { IntegrationManifest } from "../../../src/data/integration";
|
||||
import type { ConfigEntry } from "../../../src/data/config_entries";
|
||||
import type { Helper } from "../../../src/panels/config/helpers/const";
|
||||
|
||||
describe("isDeletableEntity", () => {
|
||||
it("should return true for restored entities", () => {
|
||||
const hass = {
|
||||
states: { "light.test": { attributes: { restored: true } } },
|
||||
} as unknown as HomeAssistant;
|
||||
const result = isDeletableEntity(hass, "light.test", [], [], [], []);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for non-restored entities without config entry", () => {
|
||||
const hass = {
|
||||
states: { "light.test": { attributes: {} } },
|
||||
} as unknown as HomeAssistant;
|
||||
const entityRegistry = [
|
||||
{ entity_id: "light.test" },
|
||||
] as EntityRegistryEntry[];
|
||||
const result = isDeletableEntity(
|
||||
hass,
|
||||
"light.test",
|
||||
[],
|
||||
entityRegistry,
|
||||
[],
|
||||
[]
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for helper domain entities", () => {
|
||||
const hass = {
|
||||
states: { "input_boolean.test": { attributes: {} } },
|
||||
config: { components: ["input_boolean"] },
|
||||
} as unknown as HomeAssistant;
|
||||
const entityRegistry = [
|
||||
{ entity_id: "input_boolean.test", unique_id: "123" },
|
||||
] as EntityRegistryEntry[];
|
||||
const fetchedHelpers = [{ id: "123" }] as Helper[];
|
||||
const result = isDeletableEntity(
|
||||
hass,
|
||||
"input_boolean.test",
|
||||
[],
|
||||
entityRegistry,
|
||||
[],
|
||||
fetchedHelpers
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for non-helper domain entities without restored attribute", () => {
|
||||
const hass = {
|
||||
states: { "light.test": { attributes: {} } },
|
||||
} as unknown as HomeAssistant;
|
||||
const entityRegistry = [
|
||||
{ entity_id: "light.test" },
|
||||
] as EntityRegistryEntry[];
|
||||
const result = isDeletableEntity(
|
||||
hass,
|
||||
"light.test",
|
||||
[],
|
||||
entityRegistry,
|
||||
[],
|
||||
[]
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for entities with helper integration type", () => {
|
||||
const hass = {
|
||||
states: { "light.test": { attributes: {} } },
|
||||
} as unknown as HomeAssistant;
|
||||
const entityRegistry = [
|
||||
{ entity_id: "light.test", config_entry_id: "config_1" },
|
||||
] as EntityRegistryEntry[];
|
||||
const configEntries = [
|
||||
{ entry_id: "config_1", domain: "light" },
|
||||
] as ConfigEntry[];
|
||||
const manifests = [
|
||||
{ domain: "light", integration_type: "helper" },
|
||||
] as IntegrationManifest[];
|
||||
const result = isDeletableEntity(
|
||||
hass,
|
||||
"light.test",
|
||||
manifests,
|
||||
entityRegistry,
|
||||
configEntries,
|
||||
[]
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteEntity", () => {
|
||||
it("should call removeEntityRegistryEntry for restored entities", () => {
|
||||
const removeEntityRegistryEntry = vi.fn();
|
||||
const hass = {
|
||||
states: { "light.test": { attributes: { restored: true } } },
|
||||
callWS: removeEntityRegistryEntry,
|
||||
} as unknown as HomeAssistant;
|
||||
const entityRegistry = [
|
||||
{ entity_id: "light.test" },
|
||||
] as EntityRegistryEntry[];
|
||||
deleteEntity(hass, "light.test", [], entityRegistry, [], []);
|
||||
expect(removeEntityRegistryEntry).toHaveBeenCalledWith({
|
||||
type: "config/entity_registry/remove",
|
||||
entity_id: "light.test",
|
||||
});
|
||||
});
|
||||
|
||||
it("should call deleteConfigEntry for entities with helper integration type", () => {
|
||||
const deleteConfigEntry = vi.fn();
|
||||
const hass = {
|
||||
states: { "light.test": { attributes: {} } },
|
||||
callApi: deleteConfigEntry,
|
||||
} as unknown as HomeAssistant;
|
||||
const entityRegistry = [
|
||||
{ entity_id: "light.test", config_entry_id: "config_1" },
|
||||
] as EntityRegistryEntry[];
|
||||
const configEntries = [
|
||||
{ entry_id: "config_1", domain: "light" },
|
||||
] as ConfigEntry[];
|
||||
const manifests = [
|
||||
{ domain: "light", integration_type: "helper" },
|
||||
] as IntegrationManifest[];
|
||||
deleteEntity(
|
||||
hass,
|
||||
"light.test",
|
||||
manifests,
|
||||
entityRegistry,
|
||||
configEntries,
|
||||
[]
|
||||
);
|
||||
expect(deleteConfigEntry).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should call HELPERS_CRUD.delete for helper domain entities", () => {
|
||||
const deleteCall = vi.fn();
|
||||
const hass = {
|
||||
states: { "input_boolean.test": { attributes: {} } },
|
||||
config: { components: ["input_boolean"] },
|
||||
callWS: deleteCall,
|
||||
} as unknown as HomeAssistant;
|
||||
const entityRegistry = [
|
||||
{ entity_id: "input_boolean.test", unique_id: "123" },
|
||||
] as EntityRegistryEntry[];
|
||||
const fetchedHelpers = [{ id: "123" }] as Helper[];
|
||||
deleteEntity(
|
||||
hass,
|
||||
"input_boolean.test",
|
||||
[],
|
||||
entityRegistry,
|
||||
[],
|
||||
fetchedHelpers
|
||||
);
|
||||
expect(deleteCall).toHaveBeenCalledWith({
|
||||
type: "input_boolean/delete",
|
||||
input_boolean_id: "123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should call removeEntityRegistryEntry for helper domain entities", () => {
|
||||
const removeEntityRegistryEntry = vi.fn();
|
||||
const hass = {
|
||||
states: { "input_boolean.test": { attributes: { restored: true } } },
|
||||
config: { components: ["input_boolean"] },
|
||||
callWS: removeEntityRegistryEntry,
|
||||
} as unknown as HomeAssistant;
|
||||
const entityRegistry = [
|
||||
{ entity_id: "input_boolean.test", unique_id: "124" },
|
||||
] as EntityRegistryEntry[];
|
||||
const fetchedHelpers = [{ id: "123" }] as Helper[];
|
||||
deleteEntity(
|
||||
hass,
|
||||
"input_boolean.test",
|
||||
[],
|
||||
entityRegistry,
|
||||
[],
|
||||
fetchedHelpers
|
||||
);
|
||||
expect(removeEntityRegistryEntry).toHaveBeenCalledWith({
|
||||
type: "config/entity_registry/remove",
|
||||
entity_id: "input_boolean.test",
|
||||
});
|
||||
});
|
||||
});
|
50
test/util/cache-manager.test.ts
Normal file
50
test/util/cache-manager.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { CacheManager } from "../../src/util/cache-manager";
|
||||
|
||||
const savedSetTimeout = setTimeout;
|
||||
|
||||
describe("cache-manager", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
window.setTimeout = setTimeout;
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
window.setTimeout = savedSetTimeout;
|
||||
});
|
||||
it("should return value before expiration", async () => {
|
||||
const cacheManager = new CacheManager<string>(1000);
|
||||
cacheManager.set("key", "value");
|
||||
|
||||
expect(cacheManager.has("key")).toBe(true);
|
||||
expect(cacheManager.get("key")).toBe("value");
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(cacheManager.has("key")).toBe(true);
|
||||
expect(cacheManager.get("key")).toBe("value");
|
||||
});
|
||||
|
||||
it("should not return value after expiration", async () => {
|
||||
const cacheManager = new CacheManager<string>(1000);
|
||||
cacheManager.set("key", "value");
|
||||
|
||||
expect(cacheManager.has("key")).toBe(true);
|
||||
expect(cacheManager.get("key")).toBe("value");
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(cacheManager.has("key")).toBe(false);
|
||||
expect(cacheManager.get("key")).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should always return value if no expiration", async () => {
|
||||
const cacheManager = new CacheManager<string>();
|
||||
cacheManager.set("key", "value");
|
||||
|
||||
expect(cacheManager.has("key")).toBe(true);
|
||||
expect(cacheManager.get("key")).toBe("value");
|
||||
|
||||
vi.advanceTimersByTime(10000000000000000000000);
|
||||
expect(cacheManager.has("key")).toBe(true);
|
||||
expect(cacheManager.get("key")).toBe("value");
|
||||
});
|
||||
});
|
@@ -6,7 +6,6 @@ describe("ha-pref-storage", () => {
|
||||
const mockHass = {
|
||||
dockedSidebar: "auto",
|
||||
selectedTheme: { theme: "default" },
|
||||
vibrate: "false",
|
||||
unknownKey: "unknownValue",
|
||||
};
|
||||
|
||||
@@ -25,11 +24,15 @@ describe("ha-pref-storage", () => {
|
||||
window.localStorage.setItem = vi.fn();
|
||||
|
||||
storeState(mockHass as unknown as HomeAssistant);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledTimes(7);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledTimes(8);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
"dockedSidebar",
|
||||
JSON.stringify("auto")
|
||||
);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
"selectedTheme",
|
||||
JSON.stringify({ theme: "default" })
|
||||
);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
"selectedLanguage",
|
||||
JSON.stringify(null)
|
||||
@@ -38,19 +41,13 @@ describe("ha-pref-storage", () => {
|
||||
"unknownKey",
|
||||
JSON.stringify("unknownValue")
|
||||
);
|
||||
|
||||
// browserThemeEnabled is not set in mockHass, so selectedTheme should not be stored
|
||||
expect(window.localStorage.setItem).not.toHaveBeenCalledWith(
|
||||
"selectedTheme",
|
||||
JSON.stringify({ theme: "default" })
|
||||
);
|
||||
});
|
||||
|
||||
test("storeState fails", async () => {
|
||||
const { storeState } = await import("../../src/util/ha-pref-storage");
|
||||
|
||||
window.localStorage.setItem = vi.fn((key) => {
|
||||
if (key === "selectedLanguage") {
|
||||
if (key === "selectedTheme") {
|
||||
throw new Error("Test error");
|
||||
}
|
||||
});
|
||||
@@ -68,12 +65,12 @@ describe("ha-pref-storage", () => {
|
||||
JSON.stringify("auto")
|
||||
);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
"selectedLanguage",
|
||||
JSON.stringify(null)
|
||||
"selectedTheme",
|
||||
JSON.stringify({ theme: "default" })
|
||||
);
|
||||
expect(window.localStorage.setItem).not.toHaveBeenCalledWith(
|
||||
"vibrate",
|
||||
JSON.stringify("false")
|
||||
"selectedLanguage",
|
||||
JSON.stringify(null)
|
||||
);
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.warn).toHaveBeenCalledOnce();
|
||||
@@ -90,7 +87,7 @@ describe("ha-pref-storage", () => {
|
||||
|
||||
window.localStorage.setItem("selectedTheme", JSON.stringify("test"));
|
||||
window.localStorage.setItem("dockedSidebar", JSON.stringify(true));
|
||||
window.localStorage.setItem("selectedLanguage", JSON.stringify("de"));
|
||||
window.localStorage.setItem("selectedLanguage", JSON.stringify("german"));
|
||||
|
||||
// should not be in state
|
||||
window.localStorage.setItem("testEntry", JSON.stringify("this is a test"));
|
||||
@@ -99,21 +96,7 @@ describe("ha-pref-storage", () => {
|
||||
expect(state).toEqual({
|
||||
dockedSidebar: "docked",
|
||||
selectedTheme: { theme: "test" },
|
||||
browserThemeEnabled: true,
|
||||
selectedLanguage: "de",
|
||||
});
|
||||
});
|
||||
|
||||
test("getState without theme", async () => {
|
||||
const { getState } = await import("../../src/util/ha-pref-storage");
|
||||
|
||||
window.localStorage.setItem("dockedSidebar", JSON.stringify(true));
|
||||
window.localStorage.setItem("selectedLanguage", JSON.stringify("de"));
|
||||
|
||||
const state = getState();
|
||||
expect(state).toEqual({
|
||||
dockedSidebar: "docked",
|
||||
selectedLanguage: "de",
|
||||
selectedLanguage: "german",
|
||||
});
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user