mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-06 07:57:46 +00:00
Merge branch 'dev' of github.com:home-assistant/frontend into user-siderbar
This commit is contained in:
commit
188e82fa02
@ -5,7 +5,7 @@
|
|||||||
"context": ".."
|
"context": ".."
|
||||||
},
|
},
|
||||||
"appPort": "8124:8123",
|
"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",
|
"postStartCommand": "script/bootstrap",
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"DEV_CONTAINER": "1",
|
"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."
|
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Build resources
|
- name: Build resources
|
||||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||||
- name: Setup lint cache
|
- name: Setup lint cache
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.1
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
node_modules/.cache/prettier
|
node_modules/.cache/prettier
|
||||||
@ -89,7 +89,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@v4.6.0
|
uses: actions/upload-artifact@v4.6.1
|
||||||
with:
|
with:
|
||||||
name: frontend-bundle-stats
|
name: frontend-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
@ -113,7 +113,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@v4.6.0
|
uses: actions/upload-artifact@v4.6.1
|
||||||
with:
|
with:
|
||||||
name: supervisor-bundle-stats
|
name: supervisor-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@ -57,14 +57,14 @@ jobs:
|
|||||||
run: tar -czvf translations.tar.gz translations
|
run: tar -czvf translations.tar.gz translations
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4.6.0
|
uses: actions/upload-artifact@v4.6.1
|
||||||
with:
|
with:
|
||||||
name: wheels
|
name: wheels
|
||||||
path: dist/home_assistant_frontend*.whl
|
path: dist/home_assistant_frontend*.whl
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@v4.6.0
|
uses: actions/upload-artifact@v4.6.1
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
|
42
.vscode/tasks.json
vendored
42
.vscode/tasks.json
vendored
@ -1,6 +1,42 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"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",
|
"label": "Develop Frontend",
|
||||||
"type": "gulp",
|
"type": "gulp",
|
||||||
@ -241,6 +277,12 @@
|
|||||||
"id": "supervisorToken",
|
"id": "supervisorToken",
|
||||||
"type": "promptString",
|
"type": "promptString",
|
||||||
"description": "The token for the Remote API proxy add-on"
|
"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,8 +1,9 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
import rootConfig from "../eslint.config.mjs";
|
import rootConfig from "../eslint.config.mjs";
|
||||||
|
|
||||||
export default [
|
export default tseslint.config(...rootConfig, {
|
||||||
...rootConfig,
|
|
||||||
{
|
|
||||||
rules: {
|
rules: {
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"import/no-extraneous-dependencies": "off",
|
"import/no-extraneous-dependencies": "off",
|
||||||
@ -12,5 +13,4 @@ export default [
|
|||||||
"@typescript-eslint/no-require-imports": "off",
|
"@typescript-eslint/no-require-imports": "off",
|
||||||
"prefer-arrow-callback": "off",
|
"prefer-arrow-callback": "off",
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
|
||||||
|
@ -90,6 +90,14 @@ function copyMapPanel(staticDir) {
|
|||||||
npmPath("leaflet/dist/leaflet.css"),
|
npmPath("leaflet/dist/leaflet.css"),
|
||||||
staticPath("images/leaflet/")
|
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(
|
fs.copySync(
|
||||||
npmPath("leaflet/dist/images"),
|
npmPath("leaflet/dist/images"),
|
||||||
staticPath("images/leaflet/images/")
|
staticPath("images/leaflet/images/")
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
/* eslint-disable import/no-extraneous-dependencies */
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
import unusedImports from "eslint-plugin-unused-imports";
|
import unusedImports from "eslint-plugin-unused-imports";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import tsParser from "@typescript-eslint/parser";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
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 _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = path.dirname(_filename);
|
const _dirname = path.dirname(_filename);
|
||||||
@ -15,17 +20,14 @@ const compat = new FlatCompat({
|
|||||||
allConfig: js.configs.all,
|
allConfig: js.configs.all,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default [
|
export default tseslint.config(
|
||||||
...compat.extends(
|
...compat.extends("airbnb-base", "plugin:lit-a11y/recommended"),
|
||||||
"airbnb-base",
|
eslintConfigPrettier,
|
||||||
"plugin:@typescript-eslint/recommended",
|
litConfigs["flat/all"],
|
||||||
"plugin:@typescript-eslint/strict",
|
tseslint.configs.recommended,
|
||||||
"plugin:@typescript-eslint/stylistic",
|
tseslint.configs.strict,
|
||||||
"plugin:wc/recommended",
|
tseslint.configs.stylistic,
|
||||||
"plugin:lit/all",
|
wcConfigs["flat/recommended"],
|
||||||
"plugin:lit-a11y/recommended",
|
|
||||||
"prettier"
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
plugins: {
|
plugins: {
|
||||||
"unused-imports": unusedImports,
|
"unused-imports": unusedImports,
|
||||||
@ -43,7 +45,7 @@ export default [
|
|||||||
Polymer: true,
|
Polymer: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
parser: tsParser,
|
parser: tseslint.parser,
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
|
|
||||||
@ -184,5 +186,5 @@ export default [
|
|||||||
],
|
],
|
||||||
"no-use-before-define": "off",
|
"no-use-before-define": "off",
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
];
|
);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
import rootConfig from "../eslint.config.mjs";
|
import rootConfig from "../eslint.config.mjs";
|
||||||
|
|
||||||
export default [
|
export default tseslint.config(...rootConfig, {
|
||||||
...rootConfig,
|
|
||||||
{
|
|
||||||
rules: {
|
rules: {
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
];
|
|
||||||
|
@ -17,6 +17,7 @@ import "../../../src/components/ha-alert";
|
|||||||
import {
|
import {
|
||||||
ALTERNATIVE_DNS_SERVERS,
|
ALTERNATIVE_DNS_SERVERS,
|
||||||
getSupervisorNetworkInfo,
|
getSupervisorNetworkInfo,
|
||||||
|
pingSupervisor,
|
||||||
setSupervisorNetworkDns,
|
setSupervisorNetworkDns,
|
||||||
} from "../data/supervisor";
|
} from "../data/supervisor";
|
||||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
@ -85,7 +86,28 @@ class LandingPageNetwork extends LitElement {
|
|||||||
|
|
||||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||||
super.firstUpdated(_changedProperties);
|
super.firstUpdated(_changedProperties);
|
||||||
|
this._pingSupervisor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _schedulePingSupervisor() {
|
||||||
|
setTimeout(
|
||||||
|
() => this._pingSupervisor(),
|
||||||
|
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _pingSupervisor() {
|
||||||
|
try {
|
||||||
|
const response = await pingSupervisor();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to ping supervisor, assume update in progress");
|
||||||
|
}
|
||||||
this._fetchSupervisorInfo();
|
this._fetchSupervisorInfo();
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
this._schedulePingSupervisor();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _scheduleFetchSupervisorInfo() {
|
private _scheduleFetchSupervisorInfo() {
|
||||||
|
@ -18,7 +18,7 @@ export const ALTERNATIVE_DNS_SERVERS: {
|
|||||||
];
|
];
|
||||||
|
|
||||||
export async function getSupervisorLogs(lines = 100) {
|
export async function getSupervisorLogs(lines = 100) {
|
||||||
return fetch(`/supervisor/supervisor/logs?lines=${lines}`, {
|
return fetch(`/supervisor-api/supervisor/logs?lines=${lines}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "text/plain",
|
Accept: "text/plain",
|
||||||
},
|
},
|
||||||
@ -26,22 +26,26 @@ export async function getSupervisorLogs(lines = 100) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getSupervisorLogsFollow(lines = 500) {
|
export async function getSupervisorLogsFollow(lines = 500) {
|
||||||
return fetch(`/supervisor/supervisor/logs/follow?lines=${lines}`, {
|
return fetch(`/supervisor-api/supervisor/logs/follow?lines=${lines}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "text/plain",
|
Accept: "text/plain",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function pingSupervisor() {
|
||||||
|
return fetch("/supervisor-api/supervisor/ping");
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSupervisorNetworkInfo() {
|
export async function getSupervisorNetworkInfo() {
|
||||||
return fetch("/supervisor/network/info");
|
return fetch("/supervisor-api/network/info");
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setSupervisorNetworkDns = async (
|
export const setSupervisorNetworkDns = async (
|
||||||
dnsServerIndex: number,
|
dnsServerIndex: number,
|
||||||
primaryInterface: string
|
primaryInterface: string
|
||||||
) =>
|
) =>
|
||||||
fetch(`/supervisor/network/interface/${primaryInterface}/update`, {
|
fetch(`/supervisor-api/network/interface/${primaryInterface}/update`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
ipv4: {
|
ipv4: {
|
||||||
|
44
package.json
44
package.json
@ -26,7 +26,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.26.7",
|
"@babel/runtime": "7.26.9",
|
||||||
"@braintree/sanitize-url": "7.1.1",
|
"@braintree/sanitize-url": "7.1.1",
|
||||||
"@codemirror/autocomplete": "6.18.6",
|
"@codemirror/autocomplete": "6.18.6",
|
||||||
"@codemirror/commands": "6.8.0",
|
"@codemirror/commands": "6.8.0",
|
||||||
@ -34,7 +34,7 @@
|
|||||||
"@codemirror/legacy-modes": "6.4.3",
|
"@codemirror/legacy-modes": "6.4.3",
|
||||||
"@codemirror/search": "6.5.9",
|
"@codemirror/search": "6.5.9",
|
||||||
"@codemirror/state": "6.5.2",
|
"@codemirror/state": "6.5.2",
|
||||||
"@codemirror/view": "6.36.2",
|
"@codemirror/view": "6.36.3",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.17.3",
|
"@formatjs/intl-datetimeformat": "6.17.3",
|
||||||
"@formatjs/intl-displaynames": "6.8.10",
|
"@formatjs/intl-displaynames": "6.8.10",
|
||||||
@ -53,9 +53,9 @@
|
|||||||
"@fullcalendar/timegrid": "6.1.15",
|
"@fullcalendar/timegrid": "6.1.15",
|
||||||
"@lezer/highlight": "1.2.1",
|
"@lezer/highlight": "1.2.1",
|
||||||
"@lit-labs/context": "0.4.1",
|
"@lit-labs/context": "0.4.1",
|
||||||
"@lit-labs/motion": "1.0.7",
|
"@lit-labs/motion": "1.0.8",
|
||||||
"@lit-labs/observers": "2.0.4",
|
"@lit-labs/observers": "2.0.5",
|
||||||
"@lit-labs/virtualizer": "2.0.15",
|
"@lit-labs/virtualizer": "2.1.0",
|
||||||
"@lrnwebcomponents/simple-tooltip": "8.0.2",
|
"@lrnwebcomponents/simple-tooltip": "8.0.2",
|
||||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||||
@ -92,8 +92,8 @@
|
|||||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||||
"@shoelace-style/shoelace": "2.20.0",
|
"@shoelace-style/shoelace": "2.20.0",
|
||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@vaadin/combo-box": "24.6.4",
|
"@vaadin/combo-box": "24.6.5",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.6.4",
|
"@vaadin/vaadin-themable-mixin": "24.6.5",
|
||||||
"@vibrant/color": "4.0.0",
|
"@vibrant/color": "4.0.0",
|
||||||
"@vue/web-component-wrapper": "1.3.0",
|
"@vue/web-component-wrapper": "1.3.0",
|
||||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||||
@ -121,6 +121,7 @@
|
|||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
"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": "2.8.0",
|
||||||
"lit-html": "2.8.0",
|
"lit-html": "2.8.0",
|
||||||
"luxon": "3.5.0",
|
"luxon": "3.5.0",
|
||||||
@ -154,20 +155,20 @@
|
|||||||
"xss": "1.0.15"
|
"xss": "1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.26.8",
|
"@babel/core": "7.26.9",
|
||||||
"@babel/helper-define-polyfill-provider": "0.6.3",
|
"@babel/helper-define-polyfill-provider": "0.6.3",
|
||||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
"@babel/plugin-proposal-decorators": "7.25.9",
|
||||||
"@babel/plugin-transform-runtime": "7.26.8",
|
"@babel/plugin-transform-runtime": "7.26.9",
|
||||||
"@babel/preset-env": "7.26.8",
|
"@babel/preset-env": "7.26.9",
|
||||||
"@babel/preset-typescript": "7.26.0",
|
"@babel/preset-typescript": "7.26.0",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.18.2",
|
"@bundle-stats/plugin-webpack-filter": "4.18.2",
|
||||||
"@lokalise/node-api": "13.1.0",
|
"@lokalise/node-api": "13.2.0",
|
||||||
"@octokit/auth-oauth-device": "7.1.3",
|
"@octokit/auth-oauth-device": "7.1.3",
|
||||||
"@octokit/plugin-retry": "7.1.3",
|
"@octokit/plugin-retry": "7.1.4",
|
||||||
"@octokit/rest": "21.1.0",
|
"@octokit/rest": "21.1.1",
|
||||||
"@rsdoctor/rspack-plugin": "0.4.13",
|
"@rsdoctor/rspack-plugin": "0.4.13",
|
||||||
"@rspack/cli": "1.2.3",
|
"@rspack/cli": "1.2.5",
|
||||||
"@rspack/core": "1.2.3",
|
"@rspack/core": "1.2.5",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
"@types/chromecast-caf-receiver": "6.0.21",
|
"@types/chromecast-caf-receiver": "6.0.21",
|
||||||
"@types/chromecast-caf-sender": "1.0.11",
|
"@types/chromecast-caf-sender": "1.0.11",
|
||||||
@ -177,6 +178,7 @@
|
|||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/leaflet": "1.9.16",
|
"@types/leaflet": "1.9.16",
|
||||||
"@types/leaflet-draw": "1.0.11",
|
"@types/leaflet-draw": "1.0.11",
|
||||||
|
"@types/leaflet.markercluster": "1.5.5",
|
||||||
"@types/lodash.merge": "4.6.9",
|
"@types/lodash.merge": "4.6.9",
|
||||||
"@types/luxon": "3.4.2",
|
"@types/luxon": "3.4.2",
|
||||||
"@types/mocha": "10.0.10",
|
"@types/mocha": "10.0.10",
|
||||||
@ -185,9 +187,7 @@
|
|||||||
"@types/tar": "6.1.13",
|
"@types/tar": "6.1.13",
|
||||||
"@types/ua-parser-js": "0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@typescript-eslint/eslint-plugin": "8.24.0",
|
"@vitest/coverage-v8": "3.0.6",
|
||||||
"@typescript-eslint/parser": "8.24.0",
|
|
||||||
"@vitest/coverage-v8": "3.0.5",
|
|
||||||
"babel-loader": "9.2.1",
|
"babel-loader": "9.2.1",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
@ -200,7 +200,7 @@
|
|||||||
"eslint-plugin-lit": "1.15.0",
|
"eslint-plugin-lit": "1.15.0",
|
||||||
"eslint-plugin-lit-a11y": "4.1.4",
|
"eslint-plugin-lit-a11y": "4.1.4",
|
||||||
"eslint-plugin-unused-imports": "4.1.4",
|
"eslint-plugin-unused-imports": "4.1.4",
|
||||||
"eslint-plugin-wc": "2.2.0",
|
"eslint-plugin-wc": "2.2.1",
|
||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
"fs-extra": "11.3.0",
|
"fs-extra": "11.3.0",
|
||||||
"glob": "11.0.1",
|
"glob": "11.0.1",
|
||||||
@ -226,7 +226,9 @@
|
|||||||
"terser-webpack-plugin": "5.3.11",
|
"terser-webpack-plugin": "5.3.11",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.7.3",
|
"typescript": "5.7.3",
|
||||||
"vitest": "3.0.5",
|
"typescript-eslint": "8.24.1",
|
||||||
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
|
"vitest": "3.0.6",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
"webpackbar": "7.0.0",
|
"webpackbar": "7.0.0",
|
||||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||||
@ -240,7 +242,7 @@
|
|||||||
"clean-css": "5.3.3",
|
"clean-css": "5.3.3",
|
||||||
"@lit/reactive-element": "1.6.3",
|
"@lit/reactive-element": "1.6.3",
|
||||||
"@fullcalendar/daygrid": "6.1.15",
|
"@fullcalendar/daygrid": "6.1.15",
|
||||||
"globals": "15.15.0",
|
"globals": "16.0.0",
|
||||||
"tslib": "2.8.1"
|
"tslib": "2.8.1"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.6.0"
|
"packageManager": "yarn@4.6.0"
|
||||||
|
@ -136,11 +136,18 @@ export function theme2hex(themeColor: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rgbFromColorName = colors[themeColor];
|
const rgbFromColorName = colors[themeColor];
|
||||||
if (!rgbFromColorName) {
|
if (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,
|
// We have a named color, and there's nothing in the table,
|
||||||
// so nothing further we can do with it.
|
// so nothing further we can do with it.
|
||||||
// Compare/border/background color will all be the same.
|
// Compare/border/background color will all be the same.
|
||||||
return themeColor;
|
return themeColor;
|
||||||
}
|
}
|
||||||
return rgb2hex(rgbFromColorName);
|
|
||||||
}
|
|
||||||
|
@ -16,11 +16,30 @@ export const setupLeafletMap = async (
|
|||||||
const Leaflet = (await import("leaflet")).default as LeafletModuleType;
|
const Leaflet = (await import("leaflet")).default as LeafletModuleType;
|
||||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||||
|
|
||||||
|
await import("leaflet.markercluster");
|
||||||
|
|
||||||
const map = Leaflet.map(mapElement);
|
const map = Leaflet.map(mapElement);
|
||||||
const style = document.createElement("link");
|
const style = document.createElement("link");
|
||||||
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
||||||
style.setAttribute("rel", "stylesheet");
|
style.setAttribute("rel", "stylesheet");
|
||||||
mapElement.parentNode.appendChild(style);
|
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);
|
map.setView([52.3731339, 4.8903147], 13);
|
||||||
|
|
||||||
const tileLayer = createTileLayer(Leaflet).addTo(map);
|
const tileLayer = createTileLayer(Leaflet).addTo(map);
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
export const computeDomain = (entityId: string): string =>
|
export const computeDomain = (entityId: string): string =>
|
||||||
entityId.substr(0, entityId.indexOf("."));
|
entityId.substring(0, entityId.indexOf("."));
|
||||||
|
@ -120,11 +120,6 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domain === "datetime") {
|
|
||||||
const time = new Date(state);
|
|
||||||
return formatDateTime(time, locale, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||||
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
||||||
// Attributes aren't available, we have to use `state`.
|
// Attributes aren't available, we have to use `state`.
|
||||||
@ -181,6 +176,7 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||||||
"tag",
|
"tag",
|
||||||
"tts",
|
"tts",
|
||||||
"wake_word",
|
"wake_word",
|
||||||
|
"datetime",
|
||||||
].includes(domain) ||
|
].includes(domain) ||
|
||||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||||
) {
|
) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,6 @@ import type { DataZoomComponentOption } from "echarts/components";
|
|||||||
import type { EChartsType } from "echarts/core";
|
import type { EChartsType } from "echarts/core";
|
||||||
import type {
|
import type {
|
||||||
ECElementEvent,
|
ECElementEvent,
|
||||||
SetOptionOpts,
|
|
||||||
XAXisOption,
|
XAXisOption,
|
||||||
YAXisOption,
|
YAXisOption,
|
||||||
} from "echarts/types/dist/shared";
|
} from "echarts/types/dist/shared";
|
||||||
@ -25,6 +24,7 @@ import type { HomeAssistant } from "../../types";
|
|||||||
import { isMac } from "../../util/is_mac";
|
import { isMac } from "../../util/is_mac";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import { formatTimeLabel } from "./axis-label";
|
import { formatTimeLabel } from "./axis-label";
|
||||||
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
|
|
||||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||||
|
|
||||||
@ -68,12 +68,16 @@ export class HaChartBase extends LitElement {
|
|||||||
|
|
||||||
private _listeners: (() => void)[] = [];
|
private _listeners: (() => void)[] = [];
|
||||||
|
|
||||||
|
private _originalZrFlush?: () => void;
|
||||||
|
|
||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
while (this._listeners.length) {
|
while (this._listeners.length) {
|
||||||
this._listeners.pop()!();
|
this._listeners.pop()!();
|
||||||
}
|
}
|
||||||
this.chart?.dispose();
|
this.chart?.dispose();
|
||||||
|
this.chart = undefined;
|
||||||
|
this._originalZrFlush = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
@ -86,7 +90,7 @@ export class HaChartBase extends LitElement {
|
|||||||
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
||||||
if (this._reducedMotion !== matches) {
|
if (this._reducedMotion !== matches) {
|
||||||
this._reducedMotion = matches;
|
this._reducedMotion = matches;
|
||||||
this.chart?.setOption({ animation: !this._reducedMotion });
|
this._setChartOptions({ animation: !this._reducedMotion });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -96,7 +100,7 @@ export class HaChartBase extends LitElement {
|
|||||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||||
this._modifierPressed = true;
|
this._modifierPressed = true;
|
||||||
if (!this.options?.dataZoom) {
|
if (!this.options?.dataZoom) {
|
||||||
this.chart?.setOption({ dataZoom: this._getDataZoomConfig() });
|
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -105,7 +109,7 @@ export class HaChartBase extends LitElement {
|
|||||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||||
this._modifierPressed = false;
|
this._modifierPressed = false;
|
||||||
if (!this.options?.dataZoom) {
|
if (!this.options?.dataZoom) {
|
||||||
this.chart?.setOption({ dataZoom: this._getDataZoomConfig() });
|
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -131,10 +135,8 @@ export class HaChartBase extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let chartOptions: ECOption = {};
|
let chartOptions: ECOption = {};
|
||||||
const chartUpdateParams: SetOptionOpts = { lazyUpdate: true };
|
|
||||||
if (changedProps.has("data")) {
|
if (changedProps.has("data")) {
|
||||||
chartOptions.series = this.data;
|
chartOptions.series = this.data;
|
||||||
chartUpdateParams.replaceMerge = ["series"];
|
|
||||||
}
|
}
|
||||||
if (changedProps.has("options")) {
|
if (changedProps.has("options")) {
|
||||||
chartOptions = { ...chartOptions, ...this._createOptions() };
|
chartOptions = { ...chartOptions, ...this._createOptions() };
|
||||||
@ -142,7 +144,7 @@ export class HaChartBase extends LitElement {
|
|||||||
chartOptions.dataZoom = this._getDataZoomConfig();
|
chartOptions.dataZoom = this._getDataZoomConfig();
|
||||||
}
|
}
|
||||||
if (Object.keys(chartOptions).length > 0) {
|
if (Object.keys(chartOptions).length > 0) {
|
||||||
this.chart.setOption(chartOptions, chartUpdateParams);
|
this._setChartOptions(chartOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,6 +511,31 @@ export class HaChartBase extends LitElement {
|
|||||||
return Math.max(this.clientWidth / 2, 200);
|
return Math.max(this.clientWidth / 2, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// delay the last bit of the render to avoid blocking the main thread
|
||||||
|
// this is not that impactful with sampling enabled but it doesn't hurt to have it
|
||||||
|
const zr = this.chart.getZr();
|
||||||
|
this._originalZrFlush = zr.flush;
|
||||||
|
zr.flush = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this._originalZrFlush?.call(zr);
|
||||||
|
}, 5);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const replaceMerge = options.series ? ["series"] : [];
|
||||||
|
this.chart.setOption(options, { replaceMerge });
|
||||||
|
}
|
||||||
|
|
||||||
private _handleZoomReset() {
|
private _handleZoomReset() {
|
||||||
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
||||||
}
|
}
|
||||||
|
@ -354,9 +354,10 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
name: nameY,
|
name: nameY,
|
||||||
color,
|
color,
|
||||||
symbol: "circle",
|
symbol: "circle",
|
||||||
step: "end",
|
|
||||||
animationDurationUpdate: 0,
|
|
||||||
symbolSize: 1,
|
symbolSize: 1,
|
||||||
|
step: "end",
|
||||||
|
sampling: "minmax",
|
||||||
|
animationDurationUpdate: 0,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: fill ? 0 : 1.5,
|
width: fill ? 0 : 1.5,
|
||||||
},
|
},
|
||||||
|
@ -492,8 +492,8 @@ export class StatisticsChart extends LitElement {
|
|||||||
: this.hass.localize(
|
: this.hass.localize(
|
||||||
`ui.components.statistics_charts.statistic_types.${type}`
|
`ui.components.statistics_charts.statistic_types.${type}`
|
||||||
),
|
),
|
||||||
symbol: "circle",
|
symbol: "none",
|
||||||
symbolSize: 0,
|
sampling: "minmax",
|
||||||
animationDurationUpdate: 0,
|
animationDurationUpdate: 0,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
@ -511,7 +511,6 @@ export class StatisticsChart extends LitElement {
|
|||||||
if (band && this.chartType === "line") {
|
if (band && this.chartType === "line") {
|
||||||
series.stack = `band-${statistic_id}`;
|
series.stack = `band-${statistic_id}`;
|
||||||
series.stackStrategy = "all";
|
series.stackStrategy = "all";
|
||||||
(series as LineSeriesOption).symbol = "none";
|
|
||||||
if (drawBands && type === "max") {
|
if (drawBands && type === "max") {
|
||||||
(series as LineSeriesOption).areaStyle = {
|
(series as LineSeriesOption).areaStyle = {
|
||||||
color: color + "3F",
|
color: color + "3F",
|
||||||
|
110
src/components/ha-copy-textfield.ts
Normal file
110
src/components/ha-copy-textfield.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||||
|
|
||||||
|
import "./ha-button";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
import "./ha-textfield";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||||
|
import { showToast } from "../util/toast";
|
||||||
|
import type { HaTextField } from "./ha-textfield";
|
||||||
|
|
||||||
|
@customElement("ha-copy-textfield")
|
||||||
|
export class HaCopyTextfield extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: "value" }) public value!: string;
|
||||||
|
|
||||||
|
@property({ attribute: "masked-value" }) public maskedValue?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "label" }) public label?: string;
|
||||||
|
|
||||||
|
@state() private _showMasked = true;
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div class="textfield-container">
|
||||||
|
<ha-textfield
|
||||||
|
.value=${this._showMasked && this.maskedValue
|
||||||
|
? this.maskedValue
|
||||||
|
: this.value}
|
||||||
|
readonly
|
||||||
|
.suffix=${this.maskedValue
|
||||||
|
? html`<div style="width: 24px"></div>`
|
||||||
|
: nothing}
|
||||||
|
@click=${this._focusInput}
|
||||||
|
></ha-textfield>
|
||||||
|
${this.maskedValue
|
||||||
|
? html`<ha-icon-button
|
||||||
|
class="toggle-unmasked"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
`ui.common.${this._showMasked ? "show" : "hide"}`
|
||||||
|
)}
|
||||||
|
@click=${this._toggleMasked}
|
||||||
|
.path=${this._showMasked ? mdiEye : mdiEyeOff}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
<ha-button @click=${this._copy} unelevated>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||||
|
${this.label || this.hass.localize("ui.common.copy")}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _focusInput(ev) {
|
||||||
|
const inputElement = ev.currentTarget as HaTextField;
|
||||||
|
inputElement.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleMasked(): void {
|
||||||
|
this._showMasked = !this._showMasked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _copy(): Promise<void> {
|
||||||
|
await copyToClipboard(this.value);
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textfield-container {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textfield-container ha-textfield {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-unmasked {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
inset-inline-start: initial;
|
||||||
|
inset-inline-end: 8px;
|
||||||
|
--mdc-icon-button-size: 40px;
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-copy-textfield": HaCopyTextfield;
|
||||||
|
}
|
||||||
|
}
|
@ -80,7 +80,6 @@ export class HaFormString extends LitElement implements HaFormElement {
|
|||||||
if (!this.isPassword) return nothing;
|
if (!this.isPassword) return nothing;
|
||||||
return html`
|
return html`
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
toggles
|
|
||||||
.label=${this.localize?.(
|
.label=${this.localize?.(
|
||||||
`${this.localizeBaseKey}.${
|
`${this.localizeBaseKey}.${
|
||||||
this.unmaskedPassword ? "hide_password" : "show_password"
|
this.unmaskedPassword ? "hide_password" : "show_password"
|
||||||
|
@ -132,7 +132,6 @@ export class HaPasswordField extends LitElement {
|
|||||||
@change=${this._handleChangeEvent}
|
@change=${this._handleChangeEvent}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
toggles
|
|
||||||
.label=${this.hass?.localize(
|
.label=${this.hass?.localize(
|
||||||
this._unmaskedPassword
|
this._unmaskedPassword
|
||||||
? "ui.components.selectors.text.hide_password"
|
? "ui.components.selectors.text.hide_password"
|
||||||
|
@ -95,7 +95,6 @@ export class HaTextSelector extends LitElement {
|
|||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
${this.selector.text?.type === "password"
|
${this.selector.text?.type === "password"
|
||||||
? html`<ha-icon-button
|
? html`<ha-icon-button
|
||||||
toggles
|
|
||||||
.label=${this.hass?.localize(
|
.label=${this.hass?.localize(
|
||||||
this._unmaskedPassword
|
this._unmaskedPassword
|
||||||
? "ui.components.selectors.text.hide_password"
|
? "ui.components.selectors.text.hide_password"
|
||||||
|
@ -8,9 +8,10 @@ import type {
|
|||||||
Map,
|
Map,
|
||||||
Marker,
|
Marker,
|
||||||
Polyline,
|
Polyline,
|
||||||
|
MarkerClusterGroup,
|
||||||
} from "leaflet";
|
} from "leaflet";
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { ReactiveElement, css } from "lit";
|
import { css, ReactiveElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
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 { isTouch } from "../../util/is_touch";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import "./ha-entity-marker";
|
import "./ha-entity-marker";
|
||||||
|
import { DecoratedMarker } from "../../common/map/decorated_marker";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
@ -84,6 +86,9 @@ export class HaMap extends ReactiveElement {
|
|||||||
|
|
||||||
@property({ type: Number }) public zoom = 14;
|
@property({ type: Number }) public zoom = 14;
|
||||||
|
|
||||||
|
@property({ attribute: "cluster-markers", type: Boolean })
|
||||||
|
public clusterMarkers = true;
|
||||||
|
|
||||||
@state() private _loaded = false;
|
@state() private _loaded = false;
|
||||||
|
|
||||||
public leafletMap?: Map;
|
public leafletMap?: Map;
|
||||||
@ -96,10 +101,12 @@ export class HaMap extends ReactiveElement {
|
|||||||
|
|
||||||
private _mapFocusItems: (Marker | Circle)[] = [];
|
private _mapFocusItems: (Marker | Circle)[] = [];
|
||||||
|
|
||||||
private _mapZones: (Marker | Circle)[] = [];
|
private _mapZones: DecoratedMarker[] = [];
|
||||||
|
|
||||||
private _mapFocusZones: (Marker | Circle)[] = [];
|
private _mapFocusZones: (Marker | Circle)[] = [];
|
||||||
|
|
||||||
|
private _mapCluster: MarkerClusterGroup | undefined;
|
||||||
|
|
||||||
private _mapPaths: (Polyline | CircleMarker)[] = [];
|
private _mapPaths: (Polyline | CircleMarker)[] = [];
|
||||||
|
|
||||||
private _clickCount = 0;
|
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")) {
|
if (changedProps.has("_loaded") || changedProps.has("paths")) {
|
||||||
this._drawPaths();
|
this._drawPaths();
|
||||||
}
|
}
|
||||||
@ -175,6 +186,7 @@ export class HaMap extends ReactiveElement {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._updateMapStyle();
|
this._updateMapStyle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,6 +438,11 @@ export class HaMap extends ReactiveElement {
|
|||||||
this._mapFocusZones = [];
|
this._mapFocusZones = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._mapCluster) {
|
||||||
|
this._mapCluster.remove();
|
||||||
|
this._mapCluster = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.entities) {
|
if (!this.entities) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -481,9 +498,14 @@ export class HaMap extends ReactiveElement {
|
|||||||
iconHTML = el.outerHTML;
|
iconHTML = el.outerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create marker with the icon
|
// create circle around it
|
||||||
this._mapZones.push(
|
const circle = Leaflet.circle([latitude, longitude], {
|
||||||
Leaflet.marker([latitude, longitude], {
|
interactive: false,
|
||||||
|
color: passive ? passiveZoneColor : zoneColor,
|
||||||
|
radius,
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = new DecoratedMarker([latitude, longitude], circle, {
|
||||||
icon: Leaflet.divIcon({
|
icon: Leaflet.divIcon({
|
||||||
html: iconHTML,
|
html: iconHTML,
|
||||||
iconSize: [24, 24],
|
iconSize: [24, 24],
|
||||||
@ -491,16 +513,9 @@ export class HaMap extends ReactiveElement {
|
|||||||
}),
|
}),
|
||||||
interactive: this.interactiveZones,
|
interactive: this.interactiveZones,
|
||||||
title,
|
title,
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// create circle around it
|
|
||||||
const circle = Leaflet.circle([latitude, longitude], {
|
|
||||||
interactive: false,
|
|
||||||
color: passive ? passiveZoneColor : zoneColor,
|
|
||||||
radius,
|
|
||||||
});
|
});
|
||||||
this._mapZones.push(circle);
|
|
||||||
|
this._mapZones.push(marker);
|
||||||
if (
|
if (
|
||||||
this.fitZones &&
|
this.fitZones &&
|
||||||
(typeof entity === "string" || entity.focus !== false)
|
(typeof entity === "string" || entity.focus !== false)
|
||||||
@ -538,7 +553,7 @@ export class HaMap extends ReactiveElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create marker with the icon
|
// create marker with the icon
|
||||||
const marker = Leaflet.marker([latitude, longitude], {
|
const marker = new DecoratedMarker([latitude, longitude], undefined, {
|
||||||
icon: Leaflet.divIcon({
|
icon: Leaflet.divIcon({
|
||||||
html: entityMarker,
|
html: entityMarker,
|
||||||
iconSize: [48, 48],
|
iconSize: [48, 48],
|
||||||
@ -546,24 +561,34 @@ export class HaMap extends ReactiveElement {
|
|||||||
}),
|
}),
|
||||||
title: title,
|
title: title,
|
||||||
});
|
});
|
||||||
this._mapItems.push(marker);
|
|
||||||
if (typeof entity === "string" || entity.focus !== false) {
|
if (typeof entity === "string" || entity.focus !== false) {
|
||||||
this._mapFocusItems.push(marker);
|
this._mapFocusItems.push(marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
// create circle around if entity has accuracy
|
// create circle around if entity has accuracy
|
||||||
if (gpsAccuracy) {
|
if (gpsAccuracy) {
|
||||||
this._mapItems.push(
|
marker.decorationLayer = Leaflet.circle([latitude, longitude], {
|
||||||
Leaflet.circle([latitude, longitude], {
|
|
||||||
interactive: false,
|
interactive: false,
|
||||||
color: darkPrimaryColor,
|
color: darkPrimaryColor,
|
||||||
radius: gpsAccuracy,
|
radius: gpsAccuracy,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._mapItems.push(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.clusterMarkers) {
|
||||||
|
this._mapCluster = Leaflet.markerClusterGroup({
|
||||||
|
showCoverageOnHover: false,
|
||||||
|
removeOutsideVisibleBounds: false,
|
||||||
|
maxClusterRadius: 40,
|
||||||
|
});
|
||||||
|
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));
|
this._mapZones.forEach((marker) => map.addLayer(marker));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import type { HomeAssistant } from "../types";
|
|||||||
import { fileDownload } from "../util/file_download";
|
import { fileDownload } from "../util/file_download";
|
||||||
import { domainToName } from "./integration";
|
import { domainToName } from "./integration";
|
||||||
import type { FrontendLocaleData } from "./translation";
|
import type { FrontendLocaleData } from "./translation";
|
||||||
|
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
|
||||||
import checkValidDate from "../common/datetime/check_valid_date";
|
import checkValidDate from "../common/datetime/check_valid_date";
|
||||||
import { handleFetchPromise } from "../util/hass-call-api";
|
import { handleFetchPromise } from "../util/hass-call-api";
|
||||||
|
|
||||||
@ -130,7 +131,13 @@ export interface BackupContentExtended extends BackupContent, BackupData {}
|
|||||||
|
|
||||||
export interface BackupInfo {
|
export interface BackupInfo {
|
||||||
backups: BackupContent[];
|
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 {
|
export interface BackupDetails {
|
||||||
|
@ -73,6 +73,7 @@ export interface CloudWebhook {
|
|||||||
interface CloudLoginBase {
|
interface CloudLoginBase {
|
||||||
hass: HomeAssistant;
|
hass: HomeAssistant;
|
||||||
email: string;
|
email: string;
|
||||||
|
check_connection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CloudLoginPassword extends CloudLoginBase {
|
export interface CloudLoginPassword extends CloudLoginBase {
|
||||||
|
@ -233,11 +233,11 @@ export const restoreBackup = async (
|
|||||||
type: HassioBackupDetail["type"],
|
type: HassioBackupDetail["type"],
|
||||||
backupSlug: string,
|
backupSlug: string,
|
||||||
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
||||||
useSnapshotUrl: boolean
|
useBackupUrl: boolean
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
`hassio/${useBackupUrl ? "backups" : "snapshots"}/${backupSlug}/restore/${type}`,
|
||||||
backupDetails
|
backupDetails
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -85,6 +85,7 @@ class StepFlowCreateEntry extends LitElement {
|
|||||||
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
|
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
this.navigateToResult = false;
|
||||||
this._flowDone();
|
this._flowDone();
|
||||||
showVoiceAssistantSetupDialog(this, {
|
showVoiceAssistantSetupDialog(this, {
|
||||||
deviceId: devices[0].id,
|
deviceId: devices[0].id,
|
||||||
|
@ -40,8 +40,13 @@ export class DialogEnterCode
|
|||||||
|
|
||||||
@state() private _showClearButton = false;
|
@state() private _showClearButton = false;
|
||||||
|
|
||||||
|
@state() private _narrow = false;
|
||||||
|
|
||||||
public async showDialog(dialogParams: EnterCodeDialogParams): Promise<void> {
|
public async showDialog(dialogParams: EnterCodeDialogParams): Promise<void> {
|
||||||
this._dialogParams = dialogParams;
|
this._dialogParams = dialogParams;
|
||||||
|
this._narrow = matchMedia(
|
||||||
|
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||||
|
).matches;
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +101,7 @@ export class DialogEnterCode
|
|||||||
>
|
>
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
class="input"
|
class="input"
|
||||||
dialogInitialFocus
|
?dialogInitialFocus=${!this._narrow}
|
||||||
id="code"
|
id="code"
|
||||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||||
type="password"
|
type="password"
|
||||||
@ -134,6 +139,7 @@ export class DialogEnterCode
|
|||||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||||
type="password"
|
type="password"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
|
?dialogInitialFocus=${!this._narrow}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
<div class="keypad">
|
<div class="keypad">
|
||||||
${BUTTONS.map((value) =>
|
${BUTTONS.map((value) =>
|
||||||
|
@ -49,6 +49,8 @@ class LightRgbColorPicker extends LitElement {
|
|||||||
|
|
||||||
@state() private _hsPickerValue?: [number, number];
|
@state() private _hsPickerValue?: [number, number];
|
||||||
|
|
||||||
|
@state() private _isInteracting?: boolean;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.stateObj) {
|
if (!this.stateObj) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -211,7 +213,10 @@ class LightRgbColorPicker extends LitElement {
|
|||||||
public willUpdate(changedProps: PropertyValues) {
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
if (!changedProps.has("entityId") && !changedProps.has("hass")) {
|
if (
|
||||||
|
this._isInteracting ||
|
||||||
|
(!changedProps.has("entityId") && !changedProps.has("hass"))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,10 +224,13 @@ class LightRgbColorPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _hsColorCursorMoved(ev: CustomEvent) {
|
private _hsColorCursorMoved(ev: CustomEvent) {
|
||||||
if (!ev.detail.value) {
|
const color = ev.detail.value;
|
||||||
|
this._isInteracting = color !== undefined;
|
||||||
|
|
||||||
|
if (color === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._hsPickerValue = ev.detail.value;
|
this._hsPickerValue = color;
|
||||||
|
|
||||||
this._throttleUpdateColor();
|
this._throttleUpdateColor();
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity_attributes";
|
|||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"color-changed": LightColor;
|
"color-changed": LightColor;
|
||||||
"color-hovered": LightColor | undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +53,8 @@ class LightColorTempPicker extends LitElement {
|
|||||||
|
|
||||||
@state() private _ctPickerValue?: number;
|
@state() private _ctPickerValue?: number;
|
||||||
|
|
||||||
|
@state() private _isInteracting?: boolean;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.stateObj) {
|
if (!this.stateObj) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -113,7 +114,7 @@ class LightColorTempPicker extends LitElement {
|
|||||||
public willUpdate(changedProps: PropertyValues) {
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
if (!changedProps.has("stateObj")) {
|
if (this._isInteracting || !changedProps.has("stateObj")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,16 +124,14 @@ class LightColorTempPicker extends LitElement {
|
|||||||
private _ctColorCursorMoved(ev: CustomEvent) {
|
private _ctColorCursorMoved(ev: CustomEvent) {
|
||||||
const ct = ev.detail.value;
|
const ct = ev.detail.value;
|
||||||
|
|
||||||
|
this._isInteracting = ct !== undefined;
|
||||||
|
|
||||||
if (isNaN(ct) || this._ctPickerValue === ct) {
|
if (isNaN(ct) || this._ctPickerValue === ct) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._ctPickerValue = ct;
|
this._ctPickerValue = ct;
|
||||||
|
|
||||||
fireEvent(this, "color-hovered", {
|
|
||||||
color_temp_kelvin: ct,
|
|
||||||
});
|
|
||||||
|
|
||||||
this._throttleUpdateColorTemp();
|
this._throttleUpdateColorTemp();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,8 +142,6 @@ class LightColorTempPicker extends LitElement {
|
|||||||
private _ctColorChanged(ev: CustomEvent) {
|
private _ctColorChanged(ev: CustomEvent) {
|
||||||
const ct = ev.detail.value;
|
const ct = ev.detail.value;
|
||||||
|
|
||||||
fireEvent(this, "color-hovered", undefined);
|
|
||||||
|
|
||||||
if (isNaN(ct) || this._ctPickerValue === ct) {
|
if (isNaN(ct) || this._ctPickerValue === ct) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,12 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
|||||||
this._stateObj.attributes.available_tones
|
this._stateObj.attributes.available_tones
|
||||||
).map(
|
).map(
|
||||||
([toneId, toneName]) => html`
|
([toneId, toneName]) => html`
|
||||||
<ha-list-item .value=${toneId}
|
<ha-list-item
|
||||||
|
.value=${Array.isArray(
|
||||||
|
this._stateObj!.attributes.available_tones
|
||||||
|
)
|
||||||
|
? toneName
|
||||||
|
: toneId}
|
||||||
>${toneName}</ha-list-item
|
>${toneName}</ha-list-item
|
||||||
>
|
>
|
||||||
`
|
`
|
||||||
@ -179,7 +184,7 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
|||||||
await this.hass.callService("siren", "turn_on", {
|
await this.hass.callService("siren", "turn_on", {
|
||||||
entity_id: this._stateObj!.entity_id,
|
entity_id: this._stateObj!.entity_id,
|
||||||
tone: this._tone,
|
tone: this._tone,
|
||||||
volume: this._volume,
|
volume_level: this._volume,
|
||||||
duration: this._duration,
|
duration: this._duration,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -251,6 +251,7 @@ export class QuickBar extends LitElement {
|
|||||||
<mwc-list>
|
<mwc-list>
|
||||||
${this._opened
|
${this._opened
|
||||||
? html`<lit-virtualizer
|
? html`<lit-virtualizer
|
||||||
|
tabindex="-1"
|
||||||
scroller
|
scroller
|
||||||
@keydown=${this._handleListItemKeyDown}
|
@keydown=${this._handleListItemKeyDown}
|
||||||
@rangechange=${this._handleRangeChanged}
|
@rangechange=${this._handleRangeChanged}
|
||||||
@ -326,6 +327,7 @@ export class QuickBar extends LitElement {
|
|||||||
.twoline=${Boolean(item.area)}
|
.twoline=${Boolean(item.area)}
|
||||||
.item=${item}
|
.item=${item}
|
||||||
index=${ifDefined(index)}
|
index=${ifDefined(index)}
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<span>${item.primaryText}</span>
|
<span>${item.primaryText}</span>
|
||||||
${item.area
|
${item.area
|
||||||
@ -346,6 +348,7 @@ export class QuickBar extends LitElement {
|
|||||||
.item=${item}
|
.item=${item}
|
||||||
index=${ifDefined(index)}
|
index=${ifDefined(index)}
|
||||||
graphic="icon"
|
graphic="icon"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
${item.iconPath
|
${item.iconPath
|
||||||
? html`
|
? html`
|
||||||
@ -375,6 +378,7 @@ export class QuickBar extends LitElement {
|
|||||||
index=${ifDefined(index)}
|
index=${ifDefined(index)}
|
||||||
class="command-item"
|
class="command-item"
|
||||||
hasMeta
|
hasMeta
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<ha-label
|
<ha-label
|
||||||
|
@ -10,6 +10,7 @@ import "../../../components/ha-svg-icon";
|
|||||||
import "../../../components/ha-textfield";
|
import "../../../components/ha-textfield";
|
||||||
import type { HaTextField } from "../../../components/ha-textfield";
|
import type { HaTextField } from "../../../components/ha-textfield";
|
||||||
import { cloudLogin } from "../../../data/cloud";
|
import { cloudLogin } from "../../../data/cloud";
|
||||||
|
import { showCloudAlreadyConnectedDialog } from "../../../panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
@ -25,6 +26,8 @@ export class CloudStepSignin extends LitElement {
|
|||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _checkConnection = true;
|
||||||
|
|
||||||
@query("#email", true) private _emailField!: HaTextField;
|
@query("#email", true) private _emailField!: HaTextField;
|
||||||
|
|
||||||
@query("#password", true) private _passwordField!: HaPasswordField;
|
@query("#password", true) private _passwordField!: HaPasswordField;
|
||||||
@ -115,6 +118,7 @@ export class CloudStepSignin extends LitElement {
|
|||||||
hass: this.hass,
|
hass: this.hass,
|
||||||
email: username,
|
email: username,
|
||||||
...(code ? { code } : { password }),
|
...(code ? { code } : { password }),
|
||||||
|
check_connection: this._checkConnection,
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errCode = err && err.body && err.body.code;
|
const errCode = err && err.body && err.body.code;
|
||||||
@ -139,6 +143,20 @@ export class CloudStepSignin extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (errCode === "alreadyconnectederror") {
|
||||||
|
showCloudAlreadyConnectedDialog(this, {
|
||||||
|
details: JSON.parse(err.body.message),
|
||||||
|
logInHereAction: () => {
|
||||||
|
this._checkConnection = false;
|
||||||
|
doLogin(username);
|
||||||
|
},
|
||||||
|
closeDialog: () => {
|
||||||
|
this._requestInProgress = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||||
await doLogin(username.toLowerCase());
|
await doLogin(username.toLowerCase());
|
||||||
return;
|
return;
|
||||||
|
@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||||
|
import { navigate } from "../../../../../common/navigate";
|
||||||
import "../../../../../components/ha-icon-button";
|
import "../../../../../components/ha-icon-button";
|
||||||
import "../../../../../components/ha-md-list";
|
import "../../../../../components/ha-md-list";
|
||||||
import "../../../../../components/ha-md-list-item";
|
import "../../../../../components/ha-md-list-item";
|
||||||
@ -22,7 +23,6 @@ import {
|
|||||||
import type { CloudStatus } from "../../../../../data/cloud";
|
import type { CloudStatus } from "../../../../../data/cloud";
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
import { brandsUrl } from "../../../../../util/brands-url";
|
import { brandsUrl } from "../../../../../util/brands-url";
|
||||||
import { navigate } from "../../../../../common/navigate";
|
|
||||||
|
|
||||||
const DEFAULT_AGENTS = [];
|
const DEFAULT_AGENTS = [];
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ enum BackupScheduleTime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface RetentionData {
|
interface RetentionData {
|
||||||
type: "copies" | "days";
|
type: "copies" | "days" | "forever";
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ const RETENTION_PRESETS: Record<
|
|||||||
RetentionData
|
RetentionData
|
||||||
> = {
|
> = {
|
||||||
copies_3: { type: "copies", value: 3 },
|
copies_3: { type: "copies", value: 3 },
|
||||||
forever: { type: "days", value: 0 },
|
forever: { type: "forever", value: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCHEDULE_OPTIONS = [
|
const SCHEDULE_OPTIONS = [
|
||||||
@ -79,7 +79,10 @@ const computeRetentionPreset = (
|
|||||||
data: RetentionData
|
data: RetentionData
|
||||||
): RetentionPreset | undefined => {
|
): RetentionPreset | undefined => {
|
||||||
for (const [key, value] of Object.entries(RETENTION_PRESETS)) {
|
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;
|
return key as RetentionPreset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,7 +95,7 @@ interface FormData {
|
|||||||
time?: string | null;
|
time?: string | null;
|
||||||
days: BackupDay[];
|
days: BackupDay[];
|
||||||
retention: {
|
retention: {
|
||||||
type: "copies" | "days";
|
type: "copies" | "days" | "forever";
|
||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -142,7 +145,12 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
? config.schedule.days
|
? config.schedule.days
|
||||||
: [],
|
: [],
|
||||||
retention: {
|
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,
|
value: config.retention.days ?? config.retention.copies ?? 3,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -160,7 +168,9 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
: [],
|
: [],
|
||||||
},
|
},
|
||||||
retention:
|
retention:
|
||||||
data.retention.type === "days"
|
data.retention.type === "forever"
|
||||||
|
? { days: null, copies: null }
|
||||||
|
: data.retention.type === "days"
|
||||||
? { days: data.retention.value, copies: null }
|
? { days: data.retention.value, copies: null }
|
||||||
: { copies: data.retention.value, days: null },
|
: { copies: data.retention.value, days: null },
|
||||||
};
|
};
|
||||||
@ -481,9 +491,19 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
private _retentionPresetChanged(ev) {
|
private _retentionPresetChanged(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.currentTarget as HaMdSelect;
|
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;
|
this._retentionPreset = value;
|
||||||
|
value = RetentionPreset.COPIES_3;
|
||||||
|
} else {
|
||||||
|
this._retentionPreset = value;
|
||||||
|
}
|
||||||
|
|
||||||
if (value !== RetentionPreset.CUSTOM) {
|
if (value !== RetentionPreset.CUSTOM) {
|
||||||
const data = this._getData(this.value);
|
const data = this._getData(this.value);
|
||||||
const retention = RETENTION_PRESETS[value];
|
const retention = RETENTION_PRESETS[value];
|
||||||
@ -493,7 +513,7 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
}
|
}
|
||||||
this._setData({
|
this._setData({
|
||||||
...data,
|
...data,
|
||||||
retention: RETENTION_PRESETS[value],
|
retention,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -504,6 +524,7 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
const value = parseInt(target.value);
|
const value = parseInt(target.value);
|
||||||
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
|
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
|
||||||
const data = this._getData(this.value);
|
const data = this._getData(this.value);
|
||||||
|
target.value = clamped.toString();
|
||||||
this._setData({
|
this._setData({
|
||||||
...data,
|
...data,
|
||||||
retention: {
|
retention: {
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
mdiUpload,
|
mdiUpload,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
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 { customElement, property, query, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
@ -27,6 +27,7 @@ import type {
|
|||||||
} from "../../../components/data-table/ha-data-table";
|
} from "../../../components/data-table/ha-data-table";
|
||||||
import "../../../components/ha-button";
|
import "../../../components/ha-button";
|
||||||
import "../../../components/ha-button-menu";
|
import "../../../components/ha-button-menu";
|
||||||
|
import "../../../components/ha-circular-progress";
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
import "../../../components/ha-filter-states";
|
import "../../../components/ha-filter-states";
|
||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
@ -460,7 +461,17 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
extended
|
extended
|
||||||
@click=${this._newBackup}
|
@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>
|
</ha-fab>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
@ -605,7 +616,14 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
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-button-menu";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
|
import "../../../components/ha-circular-progress";
|
||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
import "../../../components/ha-icon-next";
|
import "../../../components/ha-icon-next";
|
||||||
import "../../../components/ha-icon-overflow-menu";
|
import "../../../components/ha-icon-overflow-menu";
|
||||||
@ -17,8 +18,10 @@ import type {
|
|||||||
BackupAgent,
|
BackupAgent,
|
||||||
BackupConfig,
|
BackupConfig,
|
||||||
BackupContent,
|
BackupContent,
|
||||||
|
BackupInfo,
|
||||||
} from "../../../data/backup";
|
} from "../../../data/backup";
|
||||||
import {
|
import {
|
||||||
|
computeBackupAgentName,
|
||||||
generateBackup,
|
generateBackup,
|
||||||
generateBackupWithAutomaticSettings,
|
generateBackupWithAutomaticSettings,
|
||||||
} from "../../../data/backup";
|
} from "../../../data/backup";
|
||||||
@ -50,6 +53,8 @@ class HaConfigBackupOverview extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public manager!: ManagerStateEvent;
|
@property({ attribute: false }) public manager!: ManagerStateEvent;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public info?: BackupInfo;
|
||||||
|
|
||||||
@property({ attribute: false }) public backups: BackupContent[] = [];
|
@property({ attribute: false }) public backups: BackupContent[] = [];
|
||||||
|
|
||||||
@property({ attribute: false }) public fetching = false;
|
@property({ attribute: false }) public fetching = false;
|
||||||
@ -151,6 +156,26 @@ class HaConfigBackupOverview extends LitElement {
|
|||||||
</ha-list-item>
|
</ha-list-item>
|
||||||
</ha-button-menu>
|
</ha-button-menu>
|
||||||
<div class="content">
|
<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
|
${backupInProgress
|
||||||
? html`
|
? html`
|
||||||
<ha-backup-overview-progress
|
<ha-backup-overview-progress
|
||||||
@ -204,7 +229,14 @@ class HaConfigBackupOverview extends LitElement {
|
|||||||
extended
|
extended
|
||||||
@click=${this._newBackup}
|
@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>
|
</ha-fab>
|
||||||
</hass-subpage>
|
</hass-subpage>
|
||||||
`;
|
`;
|
||||||
@ -231,6 +263,9 @@ class HaConfigBackupOverview extends LitElement {
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 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 type { PropertyValues } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
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 "./components/config/ha-backup-config-schedule";
|
||||||
import type { BackupConfigSchedule } from "./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 { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
|
||||||
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
|
|
||||||
@customElement("ha-config-backup-settings")
|
@customElement("ha-config-backup-settings")
|
||||||
class HaConfigBackupSettings extends LitElement {
|
class HaConfigBackupSettings extends LitElement {
|
||||||
@ -98,6 +99,8 @@ class HaConfigBackupSettings extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const supervisor = isComponentLoaded(this.hass, "hassio");
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-subpage
|
<hass-subpage
|
||||||
back-path="/config/backup"
|
back-path="/config/backup"
|
||||||
@ -105,7 +108,7 @@ class HaConfigBackupSettings extends LitElement {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.header=${this.hass.localize("ui.panel.config.backup.settings.header")}
|
.header=${this.hass.localize("ui.panel.config.backup.settings.header")}
|
||||||
>
|
>
|
||||||
${isComponentLoaded(this.hass, "hassio")
|
${supervisor
|
||||||
? html`
|
? html`
|
||||||
<ha-button-menu slot="toolbar-icon">
|
<ha-button-menu slot="toolbar-icon">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
@ -203,6 +206,29 @@ class HaConfigBackupSettings extends LitElement {
|
|||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</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>
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@ -342,6 +368,9 @@ class HaConfigBackupSettings extends LitElement {
|
|||||||
.card-content {
|
.card-content {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import type {
|
import type {
|
||||||
BackupAgent,
|
BackupAgent,
|
||||||
BackupConfig,
|
BackupConfig,
|
||||||
BackupContent,
|
BackupInfo,
|
||||||
} from "../../../data/backup";
|
} from "../../../data/backup";
|
||||||
import {
|
import {
|
||||||
compareAgents,
|
compareAgents,
|
||||||
@ -44,7 +44,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
|||||||
|
|
||||||
@state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE;
|
@state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE;
|
||||||
|
|
||||||
@state() private _backups: BackupContent[] = [];
|
@state() private _info?: BackupInfo;
|
||||||
|
|
||||||
@state() private _agents: BackupAgent[] = [];
|
@state() private _agents: BackupAgent[] = [];
|
||||||
|
|
||||||
@ -87,8 +87,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchBackupInfo() {
|
private async _fetchBackupInfo() {
|
||||||
const info = await fetchBackupInfo(this.hass);
|
this._info = await fetchBackupInfo(this.hass);
|
||||||
this._backups = info.backups;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchBackupConfig() {
|
private async _fetchBackupConfig() {
|
||||||
@ -134,7 +133,8 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
|||||||
pageEl.narrow = this.narrow;
|
pageEl.narrow = this.narrow;
|
||||||
pageEl.cloudStatus = this.cloudStatus;
|
pageEl.cloudStatus = this.cloudStatus;
|
||||||
pageEl.manager = this._manager;
|
pageEl.manager = this._manager;
|
||||||
pageEl.backups = this._backups;
|
pageEl.info = this._info;
|
||||||
|
pageEl.backups = this._info?.backups || [];
|
||||||
pageEl.config = this._config;
|
pageEl.config = this._config;
|
||||||
pageEl.agents = this._agents;
|
pageEl.agents = this._agents;
|
||||||
pageEl.fetching = this._fetching;
|
pageEl.fetching = this._fetching;
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
import { mdiContentCopy, mdiEye, mdiEyeOff, mdiHelpCircle } from "@mdi/js";
|
import { mdiHelpCircle } from "@mdi/js";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
import "../../../../components/ha-expansion-panel";
|
import "../../../../components/ha-expansion-panel";
|
||||||
import "../../../../components/ha-formfield";
|
|
||||||
import "../../../../components/ha-radio";
|
|
||||||
import "../../../../components/ha-settings-row";
|
import "../../../../components/ha-settings-row";
|
||||||
import "../../../../components/ha-switch";
|
import "../../../../components/ha-switch";
|
||||||
import "../../../../components/ha-textfield";
|
|
||||||
|
|
||||||
import { formatDate } from "../../../../common/datetime/format_date";
|
import { formatDate } from "../../../../common/datetime/format_date";
|
||||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||||
@ -25,6 +21,7 @@ import type { HomeAssistant } from "../../../../types";
|
|||||||
import { showToast } from "../../../../util/toast";
|
import { showToast } from "../../../../util/toast";
|
||||||
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
|
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
|
||||||
import { obfuscateUrl } from "../../../../util/url";
|
import { obfuscateUrl } from "../../../../util/url";
|
||||||
|
import "../../../../components/ha-copy-textfield";
|
||||||
|
|
||||||
@customElement("cloud-remote-pref")
|
@customElement("cloud-remote-pref")
|
||||||
export class CloudRemotePref extends LitElement {
|
export class CloudRemotePref extends LitElement {
|
||||||
@ -34,8 +31,6 @@ export class CloudRemotePref extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public narrow = false;
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@state() private _unmaskedUrl = false;
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.cloudStatus) {
|
if (!this.cloudStatus) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -139,37 +134,13 @@ export class CloudRemotePref extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
`}
|
`}
|
||||||
<div class="url-container">
|
|
||||||
<div class="textfield-container">
|
<ha-copy-textfield
|
||||||
<ha-textfield
|
.hass=${this.hass}
|
||||||
.value=${this._unmaskedUrl
|
.value=${`https://${remote_domain}`}
|
||||||
? `https://${remote_domain}`
|
.maskedValue=${obfuscateUrl(`https://${remote_domain}`)}
|
||||||
: obfuscateUrl(`https://${remote_domain}`)}
|
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
|
||||||
readonly
|
></ha-copy-textfield>
|
||||||
.suffix=${
|
|
||||||
// reserve some space for the icon.
|
|
||||||
html`<div style="width: 24px"></div>`
|
|
||||||
}
|
|
||||||
></ha-textfield>
|
|
||||||
<ha-icon-button
|
|
||||||
class="toggle-unmasked-url"
|
|
||||||
toggles
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
`ui.panel.config.common.${this._unmaskedUrl ? "hide" : "show"}_url`
|
|
||||||
)}
|
|
||||||
@click=${this._toggleUnmaskedUrl}
|
|
||||||
.path=${this._unmaskedUrl ? mdiEyeOff : mdiEye}
|
|
||||||
></ha-icon-button>
|
|
||||||
</div>
|
|
||||||
<ha-button
|
|
||||||
.url=${`https://${remote_domain}`}
|
|
||||||
@click=${this._copyURL}
|
|
||||||
unelevated
|
|
||||||
>
|
|
||||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
|
||||||
${this.hass.localize("ui.panel.config.common.copy_link")}
|
|
||||||
</ha-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ha-expansion-panel
|
<ha-expansion-panel
|
||||||
outlined
|
outlined
|
||||||
@ -234,10 +205,6 @@ export class CloudRemotePref extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleUnmaskedUrl(): void {
|
|
||||||
this._unmaskedUrl = !this._unmaskedUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _toggleChanged(ev) {
|
private async _toggleChanged(ev) {
|
||||||
const toggle = ev.target as HaSwitch;
|
const toggle = ev.target as HaSwitch;
|
||||||
|
|
||||||
@ -268,14 +235,6 @@ export class CloudRemotePref extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _copyURL(ev): Promise<void> {
|
|
||||||
const url = ev.currentTarget.url;
|
|
||||||
await copyToClipboard(url);
|
|
||||||
showToast(this, {
|
|
||||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
.preparing {
|
.preparing {
|
||||||
padding: 0 16px 16px;
|
padding: 0 16px 16px;
|
||||||
@ -335,30 +294,6 @@ export class CloudRemotePref extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.url-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
.textfield-container {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.textfield-container ha-textfield {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.toggle-unmasked-url {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
right: 8px;
|
|
||||||
inset-inline-start: initial;
|
|
||||||
inset-inline-end: 8px;
|
|
||||||
--mdc-icon-button-size: 40px;
|
|
||||||
--mdc-icon-size: 20px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
hr {
|
hr {
|
||||||
border: none;
|
border: none;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
@ -0,0 +1,171 @@
|
|||||||
|
import type { CSSResultGroup } from "lit";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||||
|
import { formatDateTime } from "../../../../common/datetime/format_date_time";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import "../../../../components/ha-alert";
|
||||||
|
import "../../../../components/ha-button";
|
||||||
|
import "../../../../components/ha-icon-button";
|
||||||
|
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||||
|
import { haStyleDialog } from "../../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import type { CloudAlreadyConnectedParams as CloudAlreadyConnectedDialogParams } from "./show-dialog-cloud-already-connected";
|
||||||
|
import { obfuscateUrl } from "../../../../util/url";
|
||||||
|
|
||||||
|
@customElement("dialog-cloud-already-connected")
|
||||||
|
class DialogCloudAlreadyConnected extends LitElement {
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _params?: CloudAlreadyConnectedDialogParams;
|
||||||
|
|
||||||
|
@state() private _obfuscateIp = true;
|
||||||
|
|
||||||
|
public showDialog(params: CloudAlreadyConnectedDialogParams) {
|
||||||
|
this._params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
this._params?.closeDialog();
|
||||||
|
this._params = undefined;
|
||||||
|
this._obfuscateIp = true;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._params) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
const { details } = this._params;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
.heading=${createCloseHeading(
|
||||||
|
this.hass,
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.dialog_already_connected.heading"
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="intro">
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.dialog_already_connected.description"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<b>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.dialog_already_connected.other_home_assistant"
|
||||||
|
)}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
<div class="instance-details">
|
||||||
|
<div class="instance-detail">
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.dialog_already_connected.ip_address"
|
||||||
|
)}:
|
||||||
|
</span>
|
||||||
|
<div class="obfuscated">
|
||||||
|
<span>
|
||||||
|
${this._obfuscateIp
|
||||||
|
? obfuscateUrl(details.remote_ip_address)
|
||||||
|
: details.remote_ip_address}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ha-icon-button
|
||||||
|
class="toggle-unmasked-url"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
`ui.panel.config.cloud.dialog_already_connected.obfuscated_ip.${this._obfuscateIp ? "hide" : "show"}`
|
||||||
|
)}
|
||||||
|
@click=${this._toggleObfuscateIp}
|
||||||
|
.path=${this._obfuscateIp ? mdiEye : mdiEyeOff}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="instance-detail">
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.dialog_already_connected.connected_at"
|
||||||
|
)}:
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
${formatDateTime(
|
||||||
|
new Date(details.connected_at),
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ha-alert
|
||||||
|
alert-type="info"
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.dialog_already_connected.info_backups.title"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.dialog_already_connected.info_backups.description"
|
||||||
|
)}
|
||||||
|
</ha-alert>
|
||||||
|
|
||||||
|
<ha-button @click=${this.closeDialog} slot="secondaryAction">
|
||||||
|
${this.hass!.localize("ui.common.cancel")}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button @click=${this._logInHere} slot="primaryAction">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.dialog_already_connected.login_here"
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleObfuscateIp() {
|
||||||
|
this._obfuscateIp = !this._obfuscateIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _logInHere() {
|
||||||
|
this._params?.logInHereAction();
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-dialog {
|
||||||
|
--mdc-dialog-max-width: 535px;
|
||||||
|
}
|
||||||
|
.intro b {
|
||||||
|
display: block;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.instance-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.instance-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.obfuscated {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-cloud-already-connected": DialogCloudAlreadyConnected;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export interface CloudAlreadyConnectedParams {
|
||||||
|
details: {
|
||||||
|
remote_ip_address: string;
|
||||||
|
connected_at: string;
|
||||||
|
};
|
||||||
|
logInHereAction: () => void;
|
||||||
|
closeDialog: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showCloudAlreadyConnectedDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
webhookDialogParams: CloudAlreadyConnectedParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-cloud-already-connected",
|
||||||
|
dialogImport: () => import("./dialog-cloud-already-connected"),
|
||||||
|
dialogParams: webhookDialogParams,
|
||||||
|
});
|
||||||
|
};
|
@ -1,27 +1,23 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import { mdiContentCopy, mdiOpenInNew } from "@mdi/js";
|
import { mdiOpenInNew } from "@mdi/js";
|
||||||
import type { CSSResultGroup } from "lit";
|
import type { CSSResultGroup } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { query, state } from "lit/decorators";
|
import { state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
|
||||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||||
import "../../../../components/ha-textfield";
|
|
||||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
|
||||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { documentationUrl } from "../../../../util/documentation-url";
|
import { documentationUrl } from "../../../../util/documentation-url";
|
||||||
import { showToast } from "../../../../util/toast";
|
|
||||||
import type { WebhookDialogParams } from "./show-dialog-manage-cloudhook";
|
import type { WebhookDialogParams } from "./show-dialog-manage-cloudhook";
|
||||||
|
|
||||||
|
import "../../../../components/ha-copy-textfield";
|
||||||
|
|
||||||
export class DialogManageCloudhook extends LitElement {
|
export class DialogManageCloudhook extends LitElement {
|
||||||
protected hass?: HomeAssistant;
|
protected hass?: HomeAssistant;
|
||||||
|
|
||||||
@state() private _params?: WebhookDialogParams;
|
@state() private _params?: WebhookDialogParams;
|
||||||
|
|
||||||
@query("ha-textfield") _input!: HaTextField;
|
|
||||||
|
|
||||||
public showDialog(params: WebhookDialogParams) {
|
public showDialog(params: WebhookDialogParams) {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
}
|
}
|
||||||
@ -82,21 +78,12 @@ export class DialogManageCloudhook extends LitElement {
|
|||||||
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<ha-textfield
|
|
||||||
.label=${this.hass!.localize(
|
<ha-copy-textfield
|
||||||
"ui.panel.config.cloud.dialog_cloudhook.public_url"
|
.hass=${this.hass}
|
||||||
)}
|
|
||||||
.value=${cloudhook.cloudhook_url}
|
.value=${cloudhook.cloudhook_url}
|
||||||
iconTrailing
|
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
|
||||||
readOnly
|
></ha-copy-textfield>
|
||||||
@click=${this._focusInput}
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
|
||||||
@click=${this._copyUrl}
|
|
||||||
slot="trailingIcon"
|
|
||||||
.path=${mdiContentCopy}
|
|
||||||
></ha-icon-button>
|
|
||||||
</ha-textfield>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@ -137,24 +124,6 @@ export class DialogManageCloudhook extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _focusInput(ev) {
|
|
||||||
const inputElement = ev.currentTarget as HaTextField;
|
|
||||||
inputElement.select();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _copyUrl(ev): Promise<void> {
|
|
||||||
if (!this.hass) return;
|
|
||||||
ev.stopPropagation();
|
|
||||||
const inputElement = ev.target.parentElement as HaTextField;
|
|
||||||
inputElement.select();
|
|
||||||
const url = this.hass.hassUrl(inputElement.value);
|
|
||||||
|
|
||||||
await copyToClipboard(url);
|
|
||||||
showToast(this, {
|
|
||||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
@ -163,13 +132,6 @@ export class DialogManageCloudhook extends LitElement {
|
|||||||
ha-dialog {
|
ha-dialog {
|
||||||
width: 650px;
|
width: 650px;
|
||||||
}
|
}
|
||||||
ha-textfield {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
ha-textfield > ha-icon-button {
|
|
||||||
--mdc-icon-button-size: 24px;
|
|
||||||
--mdc-icon-size: 18px;
|
|
||||||
}
|
|
||||||
button.link {
|
button.link {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -28,6 +28,7 @@ import { haStyle } from "../../../../resources/styles";
|
|||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import "../../ha-config-section";
|
import "../../ha-config-section";
|
||||||
import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
|
import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
|
||||||
|
import { showCloudAlreadyConnectedDialog } from "../dialog-cloud-already-connected/show-dialog-cloud-already-connected";
|
||||||
|
|
||||||
@customElement("cloud-login")
|
@customElement("cloud-login")
|
||||||
export class CloudLogin extends LitElement {
|
export class CloudLogin extends LitElement {
|
||||||
@ -47,6 +48,8 @@ export class CloudLogin extends LitElement {
|
|||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _checkConnection = true;
|
||||||
|
|
||||||
@query("#email", true) private _emailField!: HaTextField;
|
@query("#email", true) private _emailField!: HaTextField;
|
||||||
|
|
||||||
@query("#password", true) private _passwordField!: HaPasswordField;
|
@query("#password", true) private _passwordField!: HaPasswordField;
|
||||||
@ -244,6 +247,7 @@ export class CloudLogin extends LitElement {
|
|||||||
hass: this.hass,
|
hass: this.hass,
|
||||||
email: username,
|
email: username,
|
||||||
...(code ? { code } : { password }),
|
...(code ? { code } : { password }),
|
||||||
|
check_connection: this._checkConnection,
|
||||||
});
|
});
|
||||||
this.email = "";
|
this.email = "";
|
||||||
this._password = "";
|
this._password = "";
|
||||||
@ -283,6 +287,21 @@ export class CloudLogin extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (errCode === "alreadyconnectederror") {
|
||||||
|
showCloudAlreadyConnectedDialog(this, {
|
||||||
|
details: JSON.parse(err.body.message),
|
||||||
|
logInHereAction: () => {
|
||||||
|
this._checkConnection = false;
|
||||||
|
doLogin(username);
|
||||||
|
},
|
||||||
|
closeDialog: () => {
|
||||||
|
this._requestInProgress = false;
|
||||||
|
this.email = "";
|
||||||
|
this._password = "";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (errCode === "PasswordChangeRequired") {
|
if (errCode === "PasswordChangeRequired") {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
|
@ -185,6 +185,14 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
const yamlIntegrations: IntegrationListItem[] = [];
|
const yamlIntegrations: IntegrationListItem[] = [];
|
||||||
|
|
||||||
Object.entries(i).forEach(([domain, integration]) => {
|
Object.entries(i).forEach(([domain, integration]) => {
|
||||||
|
if (
|
||||||
|
"integration_type" in integration &&
|
||||||
|
integration.integration_type === "hardware"
|
||||||
|
) {
|
||||||
|
// Ignore hardware integrations, they cannot be added via UI
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
"integration_type" in integration &&
|
"integration_type" in integration &&
|
||||||
(integration.config_flow ||
|
(integration.config_flow ||
|
||||||
|
@ -153,7 +153,6 @@ class ConfigUrlForm extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
class="toggle-unmasked-url"
|
class="toggle-unmasked-url"
|
||||||
toggles
|
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
`ui.panel.config.common.${this._unmaskedExternalUrl ? "hide" : "show"}_url`
|
`ui.panel.config.common.${this._unmaskedExternalUrl ? "hide" : "show"}_url`
|
||||||
)}
|
)}
|
||||||
@ -254,7 +253,6 @@ class ConfigUrlForm extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
class="toggle-unmasked-url"
|
class="toggle-unmasked-url"
|
||||||
toggles
|
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
`ui.panel.config.common.${this._unmaskedInternalUrl ? "hide" : "show"}_url`
|
`ui.panel.config.common.${this._unmaskedInternalUrl ? "hide" : "show"}_url`
|
||||||
)}
|
)}
|
||||||
|
@ -20,7 +20,6 @@ export class HuiCardFeatures extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div class="container">
|
|
||||||
${this.features.map(
|
${this.features.map(
|
||||||
(feature) => html`
|
(feature) => html`
|
||||||
<hui-card-feature
|
<hui-card-feature
|
||||||
@ -31,29 +30,21 @@ export class HuiCardFeatures extends LitElement {
|
|||||||
></hui-card-feature>
|
></hui-card-feature>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
--feature-color: var(--state-icon-color);
|
--feature-color: var(--state-icon-color);
|
||||||
--feature-padding: 12px;
|
|
||||||
--feature-height: 42px;
|
--feature-height: 42px;
|
||||||
--feature-border-radius: 12px;
|
--feature-border-radius: 12px;
|
||||||
--feature-button-spacing: 12px;
|
--feature-button-spacing: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
|
||||||
.container {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: var(--feature-padding);
|
gap: 12px;
|
||||||
padding-top: 0px;
|
|
||||||
gap: var(--feature-padding);
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,134 @@
|
|||||||
|
import { mdiRestore, mdiPlus, mdiMinus } from "@mdi/js";
|
||||||
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import type { TemplateResult } from "lit";
|
||||||
|
import { LitElement, html } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import "../../../components/ha-control-select";
|
||||||
|
import { UNAVAILABLE } from "../../../data/entity";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||||
|
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||||
|
import { COUNTER_ACTIONS, type CounterActionsCardFeatureConfig } from "./types";
|
||||||
|
import "../../../components/ha-control-button-group";
|
||||||
|
import "../../../components/ha-control-button";
|
||||||
|
|
||||||
|
export const supportsCounterActionsCardFeature = (stateObj: HassEntity) => {
|
||||||
|
const domain = computeDomain(stateObj.entity_id);
|
||||||
|
return domain === "counter";
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CounterButton {
|
||||||
|
translationKey: string;
|
||||||
|
icon: string;
|
||||||
|
serviceName: string;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const COUNTER_ACTIONS_BUTTON: Record<
|
||||||
|
string,
|
||||||
|
(stateObj: HassEntity) => CounterButton
|
||||||
|
> = {
|
||||||
|
increment: (stateObj) => ({
|
||||||
|
translationKey: "increment",
|
||||||
|
icon: mdiPlus,
|
||||||
|
serviceName: "increment",
|
||||||
|
disabled: parseInt(stateObj.state) === stateObj.attributes.maximum,
|
||||||
|
}),
|
||||||
|
reset: () => ({
|
||||||
|
translationKey: "reset",
|
||||||
|
icon: mdiRestore,
|
||||||
|
serviceName: "reset",
|
||||||
|
disabled: false,
|
||||||
|
}),
|
||||||
|
decrement: (stateObj) => ({
|
||||||
|
translationKey: "decrement",
|
||||||
|
icon: mdiMinus,
|
||||||
|
serviceName: "decrement",
|
||||||
|
disabled: parseInt(stateObj.state) === stateObj.attributes.minimum,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("hui-counter-actions-card-feature")
|
||||||
|
class HuiCounterActionsCardFeature
|
||||||
|
extends LitElement
|
||||||
|
implements LovelaceCardFeature
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||||
|
|
||||||
|
@state() private _config?: CounterActionsCardFeatureConfig;
|
||||||
|
|
||||||
|
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||||
|
await import(
|
||||||
|
"../editor/config-elements/hui-counter-actions-card-feature-editor"
|
||||||
|
);
|
||||||
|
return document.createElement("hui-counter-actions-card-feature-editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
static getStubConfig(): CounterActionsCardFeatureConfig {
|
||||||
|
return {
|
||||||
|
type: "counter-actions",
|
||||||
|
actions: COUNTER_ACTIONS.map((action) => action),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config: CounterActionsCardFeatureConfig): void {
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("Invalid configuration");
|
||||||
|
}
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | null {
|
||||||
|
if (
|
||||||
|
!this._config ||
|
||||||
|
!this.hass ||
|
||||||
|
!this.stateObj ||
|
||||||
|
!supportsCounterActionsCardFeature(this.stateObj)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-control-button-group>
|
||||||
|
${this._config?.actions
|
||||||
|
?.filter((action) => COUNTER_ACTIONS.includes(action))
|
||||||
|
.map((action) => {
|
||||||
|
const button = COUNTER_ACTIONS_BUTTON[action](this.stateObj!);
|
||||||
|
return html`
|
||||||
|
<ha-control-button
|
||||||
|
.entry=${button}
|
||||||
|
.label=${this.hass!.localize(
|
||||||
|
// @ts-ignore
|
||||||
|
`ui.card.counter.actions.${button.translationKey}`
|
||||||
|
)}
|
||||||
|
@click=${this._onActionTap}
|
||||||
|
.disabled=${button.disabled ||
|
||||||
|
this.stateObj?.state === UNAVAILABLE}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||||
|
</ha-control-button>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</ha-control-button-group>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onActionTap(ev): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const entry = (ev.target! as any).entry as CounterButton;
|
||||||
|
this.hass!.callService("counter", entry.serviceName, {
|
||||||
|
entity_id: this.stateObj!.entity_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = cardFeatureStyles;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-counter-actions-card-feature": HuiCounterActionsCardFeature;
|
||||||
|
}
|
||||||
|
}
|
111
src/panels/lovelace/card-features/hui-toggle-card-feature.ts
Normal file
111
src/panels/lovelace/card-features/hui-toggle-card-feature.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { mdiPowerOff, mdiPower } from "@mdi/js";
|
||||||
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import type { TemplateResult } from "lit";
|
||||||
|
import { LitElement, html } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import { stateColorCss } from "../../../common/entity/state_color";
|
||||||
|
import "../../../components/ha-control-select";
|
||||||
|
import type { ControlSelectOption } from "../../../components/ha-control-select";
|
||||||
|
import { UNAVAILABLE } from "../../../data/entity";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import type { LovelaceCardFeature } from "../types";
|
||||||
|
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||||
|
import type { ToggleCardFeatureConfig } from "./types";
|
||||||
|
import { showToast } from "../../../util/toast";
|
||||||
|
|
||||||
|
export const supportsToggleCardFeature = (stateObj: HassEntity) => {
|
||||||
|
const domain = computeDomain(stateObj.entity_id);
|
||||||
|
return ["switch", "input_boolean"].includes(domain);
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("hui-toggle-card-feature")
|
||||||
|
class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||||
|
|
||||||
|
@state() private _config?: ToggleCardFeatureConfig;
|
||||||
|
|
||||||
|
static getStubConfig(): ToggleCardFeatureConfig {
|
||||||
|
return {
|
||||||
|
type: "toggle",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config: ToggleCardFeatureConfig): void {
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("Invalid configuration");
|
||||||
|
}
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | null {
|
||||||
|
if (
|
||||||
|
!this._config ||
|
||||||
|
!this.hass ||
|
||||||
|
!this.stateObj ||
|
||||||
|
!supportsToggleCardFeature(this.stateObj)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = stateColorCss(this.stateObj);
|
||||||
|
|
||||||
|
const options = ["on", "off"].map<ControlSelectOption>((entityState) => ({
|
||||||
|
value: entityState,
|
||||||
|
label: this.hass!.formatEntityState(this.stateObj!, entityState),
|
||||||
|
path: entityState === "on" ? mdiPower : mdiPowerOff,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-control-select
|
||||||
|
.options=${options}
|
||||||
|
.value=${this.stateObj.state}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
hide-label
|
||||||
|
.ariaLabel=${this.hass.localize("ui.card.humidifier.state")}
|
||||||
|
style=${styleMap({
|
||||||
|
"--control-select-color": color,
|
||||||
|
})}
|
||||||
|
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _valueChanged(ev: CustomEvent) {
|
||||||
|
const newState = (ev.detail as any).value;
|
||||||
|
|
||||||
|
if (
|
||||||
|
newState === this.stateObj!.state &&
|
||||||
|
!this.stateObj!.attributes.assumed_state
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const service = newState === "on" ? "turn_on" : "turn_off";
|
||||||
|
const domain = computeDomain(this.stateObj!.entity_id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.hass!.callService(domain, service, {
|
||||||
|
entity_id: this.stateObj!.entity_id,
|
||||||
|
});
|
||||||
|
} catch (_err) {
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass!.localize("ui.notification_toast.action_failed", {
|
||||||
|
service: domain + "." + service,
|
||||||
|
}),
|
||||||
|
duration: 5000,
|
||||||
|
dismissable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = cardFeatureStyles;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-toggle-card-feature": HuiToggleCardFeature;
|
||||||
|
}
|
||||||
|
}
|
@ -83,6 +83,15 @@ export interface ClimatePresetModesCardFeatureConfig {
|
|||||||
preset_modes?: string[];
|
preset_modes?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const COUNTER_ACTIONS = ["increment", "reset", "decrement"] as const;
|
||||||
|
|
||||||
|
export type CounterActions = (typeof COUNTER_ACTIONS)[number];
|
||||||
|
|
||||||
|
export interface CounterActionsCardFeatureConfig {
|
||||||
|
type: "counter-actions";
|
||||||
|
actions?: CounterActions[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface SelectOptionsCardFeatureConfig {
|
export interface SelectOptionsCardFeatureConfig {
|
||||||
type: "select-options";
|
type: "select-options";
|
||||||
options?: string[];
|
options?: string[];
|
||||||
@ -101,6 +110,10 @@ export interface TargetTemperatureCardFeatureConfig {
|
|||||||
type: "target-temperature";
|
type: "target-temperature";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ToggleCardFeatureConfig {
|
||||||
|
type: "toggle";
|
||||||
|
}
|
||||||
|
|
||||||
export interface WaterHeaterOperationModesCardFeatureConfig {
|
export interface WaterHeaterOperationModesCardFeatureConfig {
|
||||||
type: "water-heater-operation-modes";
|
type: "water-heater-operation-modes";
|
||||||
operation_modes?: OperationMode[];
|
operation_modes?: OperationMode[];
|
||||||
@ -152,6 +165,7 @@ export type LovelaceCardFeatureConfig =
|
|||||||
| ClimateSwingHorizontalModesCardFeatureConfig
|
| ClimateSwingHorizontalModesCardFeatureConfig
|
||||||
| ClimateHvacModesCardFeatureConfig
|
| ClimateHvacModesCardFeatureConfig
|
||||||
| ClimatePresetModesCardFeatureConfig
|
| ClimatePresetModesCardFeatureConfig
|
||||||
|
| CounterActionsCardFeatureConfig
|
||||||
| CoverOpenCloseCardFeatureConfig
|
| CoverOpenCloseCardFeatureConfig
|
||||||
| CoverPositionCardFeatureConfig
|
| CoverPositionCardFeatureConfig
|
||||||
| CoverTiltPositionCardFeatureConfig
|
| CoverTiltPositionCardFeatureConfig
|
||||||
@ -170,6 +184,7 @@ export type LovelaceCardFeatureConfig =
|
|||||||
| SelectOptionsCardFeatureConfig
|
| SelectOptionsCardFeatureConfig
|
||||||
| TargetHumidityCardFeatureConfig
|
| TargetHumidityCardFeatureConfig
|
||||||
| TargetTemperatureCardFeatureConfig
|
| TargetTemperatureCardFeatureConfig
|
||||||
|
| ToggleCardFeatureConfig
|
||||||
| UpdateActionsCardFeatureConfig
|
| UpdateActionsCardFeatureConfig
|
||||||
| VacuumCommandsCardFeatureConfig
|
| VacuumCommandsCardFeatureConfig
|
||||||
| WaterHeaterOperationModesCardFeatureConfig;
|
| WaterHeaterOperationModesCardFeatureConfig;
|
||||||
|
@ -327,7 +327,9 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
);
|
);
|
||||||
|
|
||||||
const untrackedConsumption: BarSeriesOption["data"] = [];
|
const untrackedConsumption: BarSeriesOption["data"] = [];
|
||||||
Object.keys(consumptionData.total).forEach((time) => {
|
Object.keys(consumptionData.total)
|
||||||
|
.sort((a, b) => Number(a) - Number(b))
|
||||||
|
.forEach((time) => {
|
||||||
const ts = Number(time);
|
const ts = Number(time);
|
||||||
const value =
|
const value =
|
||||||
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
|
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
|
||||||
|
@ -291,19 +291,18 @@ export class HuiEnergyUsageGraphCard
|
|||||||
true
|
true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
// add empty dataset so compare bars are first
|
// add empty dataset so compare bars are first
|
||||||
// `stack: usage` so it doesn't take up space yet
|
// `stack: usage` so it doesn't take up space yet
|
||||||
const firstId = statIds.from_grid?.[0] ?? "placeholder";
|
|
||||||
datasets.push({
|
datasets.push({
|
||||||
id: "compare-" + firstId,
|
id: "compare-placeholder",
|
||||||
type: "bar",
|
type: "bar",
|
||||||
stack: "usage",
|
stack: energyData.statsCompare ? "compare" : "usage",
|
||||||
data: [],
|
data: [],
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
order: 0,
|
order: 0,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
datasets.push(
|
datasets.push(
|
||||||
...this._processDataSet(
|
...this._processDataSet(
|
||||||
|
@ -256,6 +256,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
|||||||
hui-card-features {
|
hui-card-features {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: none;
|
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 { HassEntities } from "home-assistant-js-websocket";
|
||||||
import type { LatLngTuple } from "leaflet";
|
import type { LatLngTuple } from "leaflet";
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
@ -72,6 +76,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
@state() private _error?: { code: string; message: string };
|
@state() private _error?: { code: string; message: string };
|
||||||
|
|
||||||
|
@state() private _clusterMarkers = true;
|
||||||
|
|
||||||
private _subscribed?: Promise<(() => Promise<void>) | undefined>;
|
private _subscribed?: Promise<(() => Promise<void>) | undefined>;
|
||||||
|
|
||||||
public setConfig(config: MapCardConfig): void {
|
public setConfig(config: MapCardConfig): void {
|
||||||
@ -170,9 +176,22 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
.autoFit=${this._config.auto_fit || false}
|
.autoFit=${this._config.auto_fit || false}
|
||||||
.fitZones=${this._config.fit_zones}
|
.fitZones=${this._config.fit_zones}
|
||||||
.themeMode=${themeMode}
|
.themeMode=${themeMode}
|
||||||
|
.clusterMarkers=${this._clusterMarkers}
|
||||||
interactive-zones
|
interactive-zones
|
||||||
render-passive
|
render-passive
|
||||||
></ha-map>
|
></ha-map>
|
||||||
|
<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
|
<ha-icon-button
|
||||||
.label=${this.hass!.localize(
|
.label=${this.hass!.localize(
|
||||||
"ui.panel.lovelace.cards.map.reset_focus"
|
"ui.panel.lovelace.cards.map.reset_focus"
|
||||||
@ -183,6 +202,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -320,6 +340,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
this._map?.fitMap();
|
this._map?.fitMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _toggleClusterMarkers() {
|
||||||
|
this._clusterMarkers = !this._clusterMarkers;
|
||||||
|
}
|
||||||
|
|
||||||
private _getColor(entityId: string): string {
|
private _getColor(entityId: string): string {
|
||||||
let color = this._colorDict[entityId];
|
let color = this._colorDict[entityId];
|
||||||
if (color) {
|
if (color) {
|
||||||
@ -464,11 +488,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon-button {
|
#buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 75px;
|
top: 75px;
|
||||||
left: 3px;
|
left: 3px;
|
||||||
outline: none;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
|
@ -107,18 +107,26 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this._error
|
${this._error
|
||||||
? html`<ha-alert
|
? html`
|
||||||
alert-type=${this._errorLevel?.toLowerCase() || "error"}
|
<ha-alert
|
||||||
>${this._error}</ha-alert
|
.alertType=${(this._errorLevel?.toLowerCase() as
|
||||||
>`
|
| "error"
|
||||||
|
| "warning") || "error"}
|
||||||
|
>
|
||||||
|
${this._error}
|
||||||
|
</ha-alert>
|
||||||
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<ha-card .header=${this._config.title}>
|
<ha-card
|
||||||
|
.header=${!this._config.text_only ? this._config.title : undefined}
|
||||||
|
class=${classMap({
|
||||||
|
"with-header": !!this._config.title,
|
||||||
|
"text-only": this._config.text_only ?? false,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<ha-markdown
|
<ha-markdown
|
||||||
cache
|
cache
|
||||||
breaks
|
breaks
|
||||||
class=${classMap({
|
|
||||||
"no-header": !this._config.title,
|
|
||||||
})}
|
|
||||||
.content=${this._templateResult?.result}
|
.content=${this._templateResult?.result}
|
||||||
></ha-markdown>
|
></ha-markdown>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
@ -135,7 +143,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
|||||||
this._tryConnect();
|
this._tryConnect();
|
||||||
}
|
}
|
||||||
const shouldBeHidden =
|
const shouldBeHidden =
|
||||||
this._templateResult &&
|
!!this._templateResult &&
|
||||||
this._config.show_empty === false &&
|
this._config.show_empty === false &&
|
||||||
this._templateResult.result.length === 0;
|
this._templateResult.result.length === 0;
|
||||||
if (shouldBeHidden !== this.hidden) {
|
if (shouldBeHidden !== this.hidden) {
|
||||||
@ -228,11 +236,19 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
ha-markdown {
|
ha-markdown {
|
||||||
padding: 0 16px 16px;
|
padding: 16px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
ha-markdown.no-header {
|
.with-header ha-markdown {
|
||||||
padding-top: 16px;
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
.text-only {
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.text-only ha-markdown {
|
||||||
|
padding: 2px 4px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -248,6 +248,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
|||||||
hui-card-features {
|
hui-card-features {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
padding: 0 12px 12px 12px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -100,10 +100,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getCardSize(): number {
|
public getCardSize(): number {
|
||||||
|
const featuresPosition =
|
||||||
|
this._config && this._featurePosition(this._config);
|
||||||
|
const featuresCount = this._config?.features?.length || 0;
|
||||||
return (
|
return (
|
||||||
1 +
|
1 +
|
||||||
(this._config?.vertical ? 1 : 0) +
|
(this._config?.vertical ? 1 : 0) +
|
||||||
(this._config?.features?.length || 0)
|
(featuresPosition === "inline" ? 0 : featuresCount)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,9 +114,16 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
const columns = 6;
|
const columns = 6;
|
||||||
let min_columns = 6;
|
let min_columns = 6;
|
||||||
let rows = 1;
|
let rows = 1;
|
||||||
if (this._config?.features?.length) {
|
const featurePosition = this._config && this._featurePosition(this._config);
|
||||||
rows += this._config.features.length;
|
const featuresCount = this._config?.features?.length || 0;
|
||||||
|
if (featuresCount) {
|
||||||
|
if (featurePosition === "inline") {
|
||||||
|
min_columns = 12;
|
||||||
|
} else {
|
||||||
|
rows += featuresCount;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this._config?.vertical) {
|
if (this._config?.vertical) {
|
||||||
rows++;
|
rows++;
|
||||||
min_columns = 3;
|
min_columns = 3;
|
||||||
@ -210,6 +220,23 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
protected render() {
|
||||||
if (!this._config || !this.hass) {
|
if (!this._config || !this.hass) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -263,6 +290,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
? this._getImageUrl(stateObj)
|
? this._getImageUrl(stateObj)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const featurePosition = this._featurePosition(this._config);
|
||||||
|
const features = this._displayedFeatures(this._config);
|
||||||
|
|
||||||
|
const containerOrientationClass =
|
||||||
|
featurePosition === "inline" ? "horizontal" : "";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card style=${styleMap(style)} class=${classMap({ active })}>
|
<ha-card style=${styleMap(style)} class=${classMap({ active })}>
|
||||||
<div
|
<div
|
||||||
@ -278,7 +311,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
>
|
>
|
||||||
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
|
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="container ${containerOrientationClass}">
|
||||||
<div class="content ${classMap(contentClasses)}">
|
<div class="content ${classMap(contentClasses)}">
|
||||||
<ha-tile-icon
|
<ha-tile-icon
|
||||||
role=${ifDefined(this._hasIconAction ? "button" : undefined)}
|
role=${ifDefined(this._hasIconAction ? "button" : undefined)}
|
||||||
@ -308,13 +341,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
.secondary=${stateDisplay}
|
.secondary=${stateDisplay}
|
||||||
></ha-tile-info>
|
></ha-tile-info>
|
||||||
</div>
|
</div>
|
||||||
${this._config.features
|
${features.length > 0
|
||||||
? html`
|
? html`
|
||||||
<hui-card-features
|
<hui-card-features
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.stateObj=${stateObj}
|
.stateObj=${stateObj}
|
||||||
.color=${this._config.color}
|
.color=${this._config.color}
|
||||||
.features=${this._config.features}
|
.features=${features}
|
||||||
></hui-card-features>
|
></hui-card-features>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
@ -372,6 +405,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
.container.horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -379,10 +416,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical {
|
.vertical {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -413,6 +452,14 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
}
|
}
|
||||||
hui-card-features {
|
hui-card-features {
|
||||||
--feature-color: var(--tile-color);
|
--feature-color: var(--tile-color);
|
||||||
|
padding: 0 12px 12px 12px;
|
||||||
|
}
|
||||||
|
.container.horizontal hui-card-features {
|
||||||
|
width: calc(50% - var(--column-gap, 0px) / 2 - 12px);
|
||||||
|
flex: none;
|
||||||
|
--feature-height: 36px;
|
||||||
|
padding: 0 12px;
|
||||||
|
padding-inline-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"],
|
ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"],
|
||||||
|
@ -336,6 +336,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
|
|||||||
export interface MarkdownCardConfig extends LovelaceCardConfig {
|
export interface MarkdownCardConfig extends LovelaceCardConfig {
|
||||||
type: "markdown";
|
type: "markdown";
|
||||||
content: string;
|
content: string;
|
||||||
|
text_only?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
card_size?: number;
|
card_size?: number;
|
||||||
entity_ids?: string | string[];
|
entity_ids?: string | string[];
|
||||||
@ -533,6 +534,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
|||||||
icon_hold_action?: ActionConfig;
|
icon_hold_action?: ActionConfig;
|
||||||
icon_double_tap_action?: ActionConfig;
|
icon_double_tap_action?: ActionConfig;
|
||||||
features?: LovelaceCardFeatureConfig[];
|
features?: LovelaceCardFeatureConfig[];
|
||||||
|
features_position?: "bottom" | "inline";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeadingCardConfig extends LovelaceCardConfig {
|
export interface HeadingCardConfig extends LovelaceCardConfig {
|
||||||
|
@ -23,8 +23,10 @@ import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
|||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||||
|
import { addCard } from "../editor/config-util";
|
||||||
import type { LovelaceCardPath } from "../editor/lovelace-path";
|
import type { LovelaceCardPath } from "../editor/lovelace-path";
|
||||||
import {
|
import {
|
||||||
|
findLovelaceContainer,
|
||||||
findLovelaceItems,
|
findLovelaceItems,
|
||||||
getLovelaceContainerPath,
|
getLovelaceContainerPath,
|
||||||
parseLovelaceCardPath,
|
parseLovelaceCardPath,
|
||||||
@ -253,14 +255,24 @@ export class HuiCardEditMode extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _duplicateCard(): void {
|
private _duplicateCard(): void {
|
||||||
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
const { cardIndex, sectionIndex } = parseLovelaceCardPath(this.path!);
|
||||||
const containerPath = getLovelaceContainerPath(this.path!);
|
const containerPath = getLovelaceContainerPath(this.path!);
|
||||||
|
const sectionConfig =
|
||||||
|
sectionIndex !== undefined
|
||||||
|
? findLovelaceContainer(this.lovelace!.config, containerPath)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const cardConfig = this._cards![cardIndex];
|
const cardConfig = this._cards![cardIndex];
|
||||||
|
|
||||||
showEditCardDialog(this, {
|
showEditCardDialog(this, {
|
||||||
lovelaceConfig: this.lovelace!.config,
|
lovelaceConfig: this.lovelace!.config,
|
||||||
saveConfig: this.lovelace!.saveConfig,
|
saveCardConfig: async (config) => {
|
||||||
path: containerPath,
|
const newConfig = addCard(this.lovelace!.config, containerPath, config);
|
||||||
|
await this.lovelace!.saveConfig(newConfig);
|
||||||
|
},
|
||||||
cardConfig,
|
cardConfig,
|
||||||
|
sectionConfig,
|
||||||
|
isNew: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,9 +278,12 @@ export class HuiCardOptions extends LitElement {
|
|||||||
const cardConfig = this._cards![cardIndex];
|
const cardConfig = this._cards![cardIndex];
|
||||||
showEditCardDialog(this, {
|
showEditCardDialog(this, {
|
||||||
lovelaceConfig: this.lovelace!.config,
|
lovelaceConfig: this.lovelace!.config,
|
||||||
saveConfig: this.lovelace!.saveConfig,
|
saveCardConfig: async (config) => {
|
||||||
path: containerPath,
|
const newConfig = addCard(this.lovelace!.config, containerPath, config);
|
||||||
|
await this.lovelace!.saveConfig(newConfig);
|
||||||
|
},
|
||||||
cardConfig,
|
cardConfig,
|
||||||
|
isNew: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import "../card-features/hui-climate-swing-modes-card-feature";
|
|||||||
import "../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
import "../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
||||||
import "../card-features/hui-climate-hvac-modes-card-feature";
|
import "../card-features/hui-climate-hvac-modes-card-feature";
|
||||||
import "../card-features/hui-climate-preset-modes-card-feature";
|
import "../card-features/hui-climate-preset-modes-card-feature";
|
||||||
|
import "../card-features/hui-counter-actions-card-feature";
|
||||||
import "../card-features/hui-cover-open-close-card-feature";
|
import "../card-features/hui-cover-open-close-card-feature";
|
||||||
import "../card-features/hui-cover-position-card-feature";
|
import "../card-features/hui-cover-position-card-feature";
|
||||||
import "../card-features/hui-cover-tilt-card-feature";
|
import "../card-features/hui-cover-tilt-card-feature";
|
||||||
@ -22,6 +23,7 @@ import "../card-features/hui-numeric-input-card-feature";
|
|||||||
import "../card-features/hui-select-options-card-feature";
|
import "../card-features/hui-select-options-card-feature";
|
||||||
import "../card-features/hui-target-temperature-card-feature";
|
import "../card-features/hui-target-temperature-card-feature";
|
||||||
import "../card-features/hui-target-humidity-card-feature";
|
import "../card-features/hui-target-humidity-card-feature";
|
||||||
|
import "../card-features/hui-toggle-card-feature";
|
||||||
import "../card-features/hui-update-actions-card-feature";
|
import "../card-features/hui-update-actions-card-feature";
|
||||||
import "../card-features/hui-vacuum-commands-card-feature";
|
import "../card-features/hui-vacuum-commands-card-feature";
|
||||||
import "../card-features/hui-water-heater-operation-modes-card-feature";
|
import "../card-features/hui-water-heater-operation-modes-card-feature";
|
||||||
@ -39,6 +41,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
|||||||
"climate-swing-horizontal-modes",
|
"climate-swing-horizontal-modes",
|
||||||
"climate-hvac-modes",
|
"climate-hvac-modes",
|
||||||
"climate-preset-modes",
|
"climate-preset-modes",
|
||||||
|
"counter-actions",
|
||||||
"cover-open-close",
|
"cover-open-close",
|
||||||
"cover-position",
|
"cover-position",
|
||||||
"cover-tilt-position",
|
"cover-tilt-position",
|
||||||
@ -57,6 +60,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
|||||||
"select-options",
|
"select-options",
|
||||||
"target-humidity",
|
"target-humidity",
|
||||||
"target-temperature",
|
"target-temperature",
|
||||||
|
"toggle",
|
||||||
"update-actions",
|
"update-actions",
|
||||||
"vacuum-commands",
|
"vacuum-commands",
|
||||||
"water-heater-operation-modes",
|
"water-heater-operation-modes",
|
||||||
|
@ -3,10 +3,10 @@ import "@material/mwc-tab/mwc-tab";
|
|||||||
import { mdiClose } from "@mdi/js";
|
import { mdiClose } from "@mdi/js";
|
||||||
import type { CSSResultGroup } from "lit";
|
import type { CSSResultGroup } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { cache } from "lit/directives/cache";
|
import { cache } from "lit/directives/cache";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import memoize from "memoize-one";
|
import memoize from "memoize-one";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||||
@ -24,6 +24,7 @@ import {
|
|||||||
computeCards,
|
computeCards,
|
||||||
computeSection,
|
computeSection,
|
||||||
} from "../../common/generate-lovelace-config";
|
} from "../../common/generate-lovelace-config";
|
||||||
|
import { addCard } from "../config-util";
|
||||||
import {
|
import {
|
||||||
findLovelaceContainer,
|
findLovelaceContainer,
|
||||||
parseLovelaceContainerPath,
|
parseLovelaceContainerPath,
|
||||||
@ -241,11 +242,24 @@ export class HuiCreateDialogCard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lovelaceConfig = this._params!.lovelaceConfig;
|
||||||
|
const containerPath = this._params!.path;
|
||||||
|
const saveConfig = this._params!.saveConfig;
|
||||||
|
|
||||||
|
const sectionConfig =
|
||||||
|
containerPath.length === 2
|
||||||
|
? findLovelaceContainer(lovelaceConfig, containerPath)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
showEditCardDialog(this, {
|
showEditCardDialog(this, {
|
||||||
lovelaceConfig: this._params!.lovelaceConfig,
|
lovelaceConfig,
|
||||||
saveConfig: this._params!.saveConfig,
|
saveCardConfig: async (newCardConfig) => {
|
||||||
path: this._params!.path,
|
const newConfig = addCard(lovelaceConfig, containerPath, newCardConfig);
|
||||||
|
await saveConfig(newConfig);
|
||||||
|
},
|
||||||
cardConfig: config,
|
cardConfig: config,
|
||||||
|
sectionConfig,
|
||||||
|
isNew: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
|
@ -13,7 +13,6 @@ import "../../../../components/ha-dialog-header";
|
|||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-icon-button";
|
||||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
|
||||||
import {
|
import {
|
||||||
getCustomCardEntry,
|
getCustomCardEntry,
|
||||||
isCustomType,
|
isCustomType,
|
||||||
@ -23,13 +22,12 @@ import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-
|
|||||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
import { haStyleDialog } from "../../../../resources/styles";
|
import { haStyleDialog } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { showToast } from "../../../../util/toast";
|
||||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||||
import "../../cards/hui-card";
|
import "../../cards/hui-card";
|
||||||
import "../../sections/hui-section";
|
import "../../sections/hui-section";
|
||||||
import { addCard, replaceCard } from "../config-util";
|
|
||||||
import { getCardDocumentationURL } from "../get-dashboard-documentation-url";
|
import { getCardDocumentationURL } from "../get-dashboard-documentation-url";
|
||||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||||
import { findLovelaceContainer } from "../lovelace-path";
|
|
||||||
import type { GUIModeChangedEvent } from "../types";
|
import type { GUIModeChangedEvent } from "../types";
|
||||||
import "./hui-card-element-editor";
|
import "./hui-card-element-editor";
|
||||||
import type { HuiCardElementEditor } from "./hui-card-element-editor";
|
import type { HuiCardElementEditor } from "./hui-card-element-editor";
|
||||||
@ -59,9 +57,7 @@ export class HuiDialogEditCard
|
|||||||
|
|
||||||
@state() private _cardConfig?: LovelaceCardConfig;
|
@state() private _cardConfig?: LovelaceCardConfig;
|
||||||
|
|
||||||
@state() private _containerConfig!:
|
@state() private _sectionConfig?: LovelaceSectionConfig;
|
||||||
| LovelaceViewConfig
|
|
||||||
| LovelaceSectionConfig;
|
|
||||||
|
|
||||||
@state() private _saving = false;
|
@state() private _saving = false;
|
||||||
|
|
||||||
@ -85,23 +81,10 @@ export class HuiDialogEditCard
|
|||||||
this._GUImode = true;
|
this._GUImode = true;
|
||||||
this._guiModeAvailable = true;
|
this._guiModeAvailable = true;
|
||||||
|
|
||||||
const containerConfig = findLovelaceContainer(
|
this._sectionConfig = this._params.sectionConfig;
|
||||||
params.lovelaceConfig,
|
|
||||||
params.path
|
|
||||||
);
|
|
||||||
|
|
||||||
if ("strategy" in containerConfig) {
|
|
||||||
throw new Error("Can't edit strategy");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._containerConfig = containerConfig;
|
|
||||||
|
|
||||||
if ("cardConfig" in params) {
|
|
||||||
this._cardConfig = params.cardConfig;
|
this._cardConfig = params.cardConfig;
|
||||||
this._dirty = true;
|
this._dirty = Boolean(this._params.isNew);
|
||||||
} else {
|
|
||||||
this._cardConfig = this._containerConfig.cards?.[params.cardIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.large = false;
|
this.large = false;
|
||||||
if (this._cardConfig && !Object.isFrozen(this._cardConfig)) {
|
if (this._cardConfig && !Object.isFrozen(this._cardConfig)) {
|
||||||
@ -156,12 +139,12 @@ export class HuiDialogEditCard
|
|||||||
};
|
};
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._params) {
|
if (!this._params || !this._cardConfig) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
let heading: string;
|
let heading: string;
|
||||||
if (this._cardConfig && this._cardConfig.type) {
|
if (this._cardConfig.type) {
|
||||||
let cardName: string | undefined;
|
let cardName: string | undefined;
|
||||||
if (isCustomType(this._cardConfig.type)) {
|
if (isCustomType(this._cardConfig.type)) {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
@ -181,13 +164,6 @@ export class HuiDialogEditCard
|
|||||||
"ui.panel.lovelace.editor.edit_card.typed_header",
|
"ui.panel.lovelace.editor.edit_card.typed_header",
|
||||||
{ type: cardName }
|
{ type: cardName }
|
||||||
);
|
);
|
||||||
} else if (!this._cardConfig) {
|
|
||||||
heading = this._containerConfig.title
|
|
||||||
? this.hass!.localize(
|
|
||||||
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
|
|
||||||
{ name: this._containerConfig.title }
|
|
||||||
)
|
|
||||||
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
|
|
||||||
} else {
|
} else {
|
||||||
heading = this.hass!.localize(
|
heading = this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.edit_card.header"
|
"ui.panel.lovelace.editor.edit_card.header"
|
||||||
@ -230,10 +206,8 @@ export class HuiDialogEditCard
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="element-editor">
|
<div class="element-editor">
|
||||||
<hui-card-element-editor
|
<hui-card-element-editor
|
||||||
.showVisibilityTab=${this._cardConfig?.type !== "conditional"}
|
.showVisibilityTab=${this._cardConfig.type !== "conditional"}
|
||||||
.sectionConfig=${this._isInSection
|
.sectionConfig=${this._sectionConfig}
|
||||||
? this._containerConfig
|
|
||||||
: undefined}
|
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.lovelace=${this._params.lovelaceConfig}
|
.lovelace=${this._params.lovelaceConfig}
|
||||||
.value=${this._cardConfig}
|
.value=${this._cardConfig}
|
||||||
@ -244,7 +218,7 @@ export class HuiDialogEditCard
|
|||||||
></hui-card-element-editor>
|
></hui-card-element-editor>
|
||||||
</div>
|
</div>
|
||||||
<div class="element-preview">
|
<div class="element-preview">
|
||||||
${this._isInSection
|
${this._sectionConfig
|
||||||
? html`
|
? html`
|
||||||
<hui-section
|
<hui-section
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -345,14 +319,10 @@ export class HuiDialogEditCard
|
|||||||
this._cardEditorEl?.focusYamlEditor();
|
this._cardEditorEl?.focusYamlEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _isInSection() {
|
|
||||||
return this._params!.path.length === 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _cardConfigInSection = memoizeOne(
|
private _cardConfigInSection = memoizeOne(
|
||||||
(cardConfig?: LovelaceCardConfig) => {
|
(cardConfig: LovelaceCardConfig) => {
|
||||||
const { cards, title, ...containerConfig } = this
|
const { cards, title, ...containerConfig } = this
|
||||||
._containerConfig as LovelaceSectionConfig;
|
._sectionConfig as LovelaceSectionConfig;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...containerConfig,
|
...containerConfig,
|
||||||
@ -411,20 +381,18 @@ export class HuiDialogEditCard
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._saving = true;
|
this._saving = true;
|
||||||
const path = this._params!.path;
|
try {
|
||||||
await this._params!.saveConfig(
|
await this._params!.saveCardConfig(this._cardConfig!);
|
||||||
"cardConfig" in this._params!
|
|
||||||
? addCard(this._params!.lovelaceConfig, path, this._cardConfig!)
|
|
||||||
: replaceCard(
|
|
||||||
this._params!.lovelaceConfig,
|
|
||||||
[...path, this._params!.cardIndex],
|
|
||||||
this._cardConfig!
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this._saving = false;
|
this._saving = false;
|
||||||
this._dirty = false;
|
this._dirty = false;
|
||||||
showSaveSuccessToast(this, this.hass);
|
showSaveSuccessToast(this, this.hass);
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast(this, {
|
||||||
|
message: err.message,
|
||||||
|
});
|
||||||
|
this._saving = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||||
|
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||||
import type { LovelaceContainerPath } from "../lovelace-path";
|
|
||||||
|
|
||||||
export type EditCardDialogParams = {
|
export interface EditCardDialogParams {
|
||||||
lovelaceConfig: LovelaceConfig;
|
lovelaceConfig: LovelaceConfig;
|
||||||
saveConfig: (config: LovelaceConfig) => void;
|
saveCardConfig: (config: LovelaceCardConfig) => void;
|
||||||
path: LovelaceContainerPath;
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
cardIndex: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
cardConfig: LovelaceCardConfig;
|
cardConfig: LovelaceCardConfig;
|
||||||
|
sectionConfig?: LovelaceSectionConfig;
|
||||||
|
isNew?: boolean;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
export const importEditCardDialog = () => import("./hui-dialog-edit-card");
|
export const importEditCardDialog = () => import("./hui-dialog-edit-card");
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-cli
|
|||||||
import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
|
import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
|
||||||
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
|
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
|
||||||
import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
||||||
|
import { supportsCounterActionsCardFeature } from "../../card-features/hui-counter-actions-card-feature";
|
||||||
import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature";
|
import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature";
|
||||||
import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature";
|
import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature";
|
||||||
import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature";
|
import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature";
|
||||||
@ -42,6 +43,7 @@ import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric
|
|||||||
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
||||||
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
|
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
|
||||||
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
||||||
|
import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-feature";
|
||||||
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
|
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
|
||||||
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
|
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
|
||||||
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
|
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
|
||||||
@ -58,6 +60,7 @@ const UI_FEATURE_TYPES = [
|
|||||||
"climate-preset-modes",
|
"climate-preset-modes",
|
||||||
"climate-swing-modes",
|
"climate-swing-modes",
|
||||||
"climate-swing-horizontal-modes",
|
"climate-swing-horizontal-modes",
|
||||||
|
"counter-actions",
|
||||||
"cover-open-close",
|
"cover-open-close",
|
||||||
"cover-position",
|
"cover-position",
|
||||||
"cover-tilt-position",
|
"cover-tilt-position",
|
||||||
@ -76,6 +79,7 @@ const UI_FEATURE_TYPES = [
|
|||||||
"select-options",
|
"select-options",
|
||||||
"target-humidity",
|
"target-humidity",
|
||||||
"target-temperature",
|
"target-temperature",
|
||||||
|
"toggle",
|
||||||
"update-actions",
|
"update-actions",
|
||||||
"vacuum-commands",
|
"vacuum-commands",
|
||||||
"water-heater-operation-modes",
|
"water-heater-operation-modes",
|
||||||
@ -90,6 +94,7 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
|||||||
"climate-preset-modes",
|
"climate-preset-modes",
|
||||||
"climate-swing-modes",
|
"climate-swing-modes",
|
||||||
"climate-swing-horizontal-modes",
|
"climate-swing-horizontal-modes",
|
||||||
|
"counter-actions",
|
||||||
"fan-preset-modes",
|
"fan-preset-modes",
|
||||||
"humidifier-modes",
|
"humidifier-modes",
|
||||||
"lawn-mower-commands",
|
"lawn-mower-commands",
|
||||||
@ -111,6 +116,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
|||||||
supportsClimateSwingHorizontalModesCardFeature,
|
supportsClimateSwingHorizontalModesCardFeature,
|
||||||
"climate-hvac-modes": supportsClimateHvacModesCardFeature,
|
"climate-hvac-modes": supportsClimateHvacModesCardFeature,
|
||||||
"climate-preset-modes": supportsClimatePresetModesCardFeature,
|
"climate-preset-modes": supportsClimatePresetModesCardFeature,
|
||||||
|
"counter-actions": supportsCounterActionsCardFeature,
|
||||||
"cover-open-close": supportsCoverOpenCloseCardFeature,
|
"cover-open-close": supportsCoverOpenCloseCardFeature,
|
||||||
"cover-position": supportsCoverPositionCardFeature,
|
"cover-position": supportsCoverPositionCardFeature,
|
||||||
"cover-tilt-position": supportsCoverTiltPositionCardFeature,
|
"cover-tilt-position": supportsCoverTiltPositionCardFeature,
|
||||||
@ -129,6 +135,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
|||||||
"select-options": supportsSelectOptionsCardFeature,
|
"select-options": supportsSelectOptionsCardFeature,
|
||||||
"target-humidity": supportsTargetHumidityCardFeature,
|
"target-humidity": supportsTargetHumidityCardFeature,
|
||||||
"target-temperature": supportsTargetTemperatureCardFeature,
|
"target-temperature": supportsTargetTemperatureCardFeature,
|
||||||
|
toggle: supportsToggleCardFeature,
|
||||||
"update-actions": supportsUpdateActionsCardFeature,
|
"update-actions": supportsUpdateActionsCardFeature,
|
||||||
"vacuum-commands": supportsVacuumCommandsCardFeature,
|
"vacuum-commands": supportsVacuumCommandsCardFeature,
|
||||||
"water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature,
|
"water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature,
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
import { html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||||
|
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||||
|
import "../../../../components/ha-form/ha-form";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import {
|
||||||
|
COUNTER_ACTIONS,
|
||||||
|
type LovelaceCardFeatureContext,
|
||||||
|
type CounterActionsCardFeatureConfig,
|
||||||
|
} from "../../card-features/types";
|
||||||
|
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||||
|
|
||||||
|
@customElement("hui-counter-actions-card-feature-editor")
|
||||||
|
export class HuiCounterActionsCardFeatureEditor
|
||||||
|
extends LitElement
|
||||||
|
implements LovelaceCardFeatureEditor
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||||
|
|
||||||
|
@state() private _config?: CounterActionsCardFeatureConfig;
|
||||||
|
|
||||||
|
public setConfig(config: CounterActionsCardFeatureConfig): void {
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _schema = memoizeOne(
|
||||||
|
(localize: LocalizeFunc) =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "actions",
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
multiple: true,
|
||||||
|
mode: "list",
|
||||||
|
reorder: true,
|
||||||
|
options: COUNTER_ACTIONS.map((action) => ({
|
||||||
|
value: action,
|
||||||
|
label: `${localize(
|
||||||
|
`ui.panel.lovelace.editor.features.types.counter-actions.actions.${action}`
|
||||||
|
)}`,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
);
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass || !this._config) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = this._schema(this.hass.localize);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-form
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${this._config}
|
||||||
|
.schema=${schema}
|
||||||
|
.computeLabel=${this._computeLabelCallback}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
|
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeLabelCallback = (
|
||||||
|
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||||
|
) => {
|
||||||
|
switch (schema.name) {
|
||||||
|
default:
|
||||||
|
return this.hass!.localize(
|
||||||
|
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-counter-actions-card-feature-editor": HuiCounterActionsCardFeatureEditor;
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +1,28 @@
|
|||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { assert, assign, object, optional, string } from "superstruct";
|
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-form/ha-form";
|
import "../../../../components/ha-form/ha-form";
|
||||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
import type {
|
||||||
|
HaFormSchema,
|
||||||
|
SchemaUnion,
|
||||||
|
} from "../../../../components/ha-form/types";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import type { MarkdownCardConfig } from "../../cards/types";
|
import type { MarkdownCardConfig } from "../../cards/types";
|
||||||
import type { LovelaceCardEditor } from "../../types";
|
import type { LovelaceCardEditor } from "../../types";
|
||||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||||
|
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||||
|
|
||||||
const cardConfigStruct = assign(
|
const cardConfigStruct = assign(
|
||||||
baseLovelaceCardConfig,
|
baseLovelaceCardConfig,
|
||||||
object({
|
object({
|
||||||
|
text_only: optional(boolean()),
|
||||||
title: optional(string()),
|
title: optional(string()),
|
||||||
content: string(),
|
content: string(),
|
||||||
theme: optional(string()),
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const SCHEMA = [
|
|
||||||
{ name: "title", selector: { text: {} } },
|
|
||||||
{ name: "content", required: true, selector: { template: {} } },
|
|
||||||
{ name: "theme", selector: { theme: {} } },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
@customElement("hui-markdown-card-editor")
|
@customElement("hui-markdown-card-editor")
|
||||||
export class HuiMarkdownCardEditor
|
export class HuiMarkdownCardEditor
|
||||||
extends LitElement
|
extends LitElement
|
||||||
@ -38,16 +37,51 @@ export class HuiMarkdownCardEditor
|
|||||||
this._config = config;
|
this._config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _schema = memoizeOne(
|
||||||
|
(localize: LocalizeFunc, text_only: boolean) =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "style",
|
||||||
|
required: true,
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
mode: "dropdown",
|
||||||
|
options: ["card", "text-only"].map((style) => ({
|
||||||
|
label: localize(
|
||||||
|
`ui.panel.lovelace.editor.card.markdown.style_options.${style}`
|
||||||
|
),
|
||||||
|
value: style,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(!text_only
|
||||||
|
? ([{ name: "title", selector: { text: {} } }] as const)
|
||||||
|
: []),
|
||||||
|
{ name: "content", required: true, selector: { template: {} } },
|
||||||
|
] as const satisfies HaFormSchema[]
|
||||||
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass || !this._config) {
|
if (!this.hass || !this._config) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...this._config,
|
||||||
|
style: this._config.text_only ? "text-only" : "card",
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema = this._schema(
|
||||||
|
this.hass.localize,
|
||||||
|
this._config.text_only || false
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-form
|
<ha-form
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.data=${this._config}
|
.data=${data}
|
||||||
.schema=${SCHEMA}
|
.schema=${schema}
|
||||||
.computeLabel=${this._computeLabelCallback}
|
.computeLabel=${this._computeLabelCallback}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></ha-form>
|
></ha-form>
|
||||||
@ -55,17 +89,23 @@ export class HuiMarkdownCardEditor
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: CustomEvent): void {
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
const config = { ...ev.detail.value };
|
||||||
|
|
||||||
|
if (config.style === "text-only") {
|
||||||
|
config.text_only = true;
|
||||||
|
} else {
|
||||||
|
delete config.text_only;
|
||||||
|
}
|
||||||
|
delete config.style;
|
||||||
|
|
||||||
|
fireEvent(this, "config-changed", { config });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
private _computeLabelCallback = (
|
||||||
|
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||||
|
) => {
|
||||||
switch (schema.name) {
|
switch (schema.name) {
|
||||||
case "theme":
|
case "style":
|
||||||
return `${this.hass!.localize(
|
|
||||||
"ui.panel.lovelace.editor.card.generic.theme"
|
|
||||||
)} (${this.hass!.localize(
|
|
||||||
"ui.panel.lovelace.editor.card.config.optional"
|
|
||||||
)})`;
|
|
||||||
case "content":
|
case "content":
|
||||||
return this.hass!.localize(
|
return this.hass!.localize(
|
||||||
`ui.panel.lovelace.editor.card.markdown.${schema.name}`
|
`ui.panel.lovelace.editor.card.markdown.${schema.name}`
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
optional,
|
optional,
|
||||||
string,
|
string,
|
||||||
} from "superstruct";
|
} from "superstruct";
|
||||||
|
import { keyed } from "lit/directives/keyed";
|
||||||
import type {
|
import type {
|
||||||
HaFormSchema,
|
HaFormSchema,
|
||||||
SchemaUnion,
|
SchemaUnion,
|
||||||
@ -84,6 +85,8 @@ export class HuiStackCardEditor
|
|||||||
|
|
||||||
@state() protected _guiModeAvailable? = true;
|
@state() protected _guiModeAvailable? = true;
|
||||||
|
|
||||||
|
protected _keys = new WeakMap<LovelaceCardConfig, string>();
|
||||||
|
|
||||||
protected _schema: readonly HaFormSchema[] = SCHEMA;
|
protected _schema: readonly HaFormSchema[] = SCHEMA;
|
||||||
|
|
||||||
@query("hui-card-element-editor")
|
@query("hui-card-element-editor")
|
||||||
@ -199,14 +202,16 @@ export class HuiStackCardEditor
|
|||||||
@click=${this._handleDeleteCard}
|
@click=${this._handleDeleteCard}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</div>
|
</div>
|
||||||
|
${keyed(
|
||||||
<hui-card-element-editor
|
this._getKey(this._config.cards[selected]),
|
||||||
|
html`<hui-card-element-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._config.cards[selected]}
|
.value=${this._config.cards[selected]}
|
||||||
.lovelace=${this.lovelace}
|
.lovelace=${this.lovelace}
|
||||||
@config-changed=${this._handleConfigChanged}
|
@config-changed=${this._handleConfigChanged}
|
||||||
@GUImode-changed=${this._handleGUIModeChanged}
|
@GUImode-changed=${this._handleGUIModeChanged}
|
||||||
></hui-card-element-editor>
|
></hui-card-element-editor>`
|
||||||
|
)}
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<hui-card-picker
|
<hui-card-picker
|
||||||
@ -220,6 +225,14 @@ export class HuiStackCardEditor
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getKey(card: LovelaceCardConfig) {
|
||||||
|
if (!this._keys.has(card)) {
|
||||||
|
this._keys.set(card, Math.random().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._keys.get(card)!;
|
||||||
|
}
|
||||||
|
|
||||||
protected _handleSelectedCard(ev) {
|
protected _handleSelectedCard(ev) {
|
||||||
if (ev.target.id === "add-card") {
|
if (ev.target.id === "add-card") {
|
||||||
this._selectedCard = this._config!.cards.length;
|
this._selectedCard = this._config!.cards.length;
|
||||||
@ -236,7 +249,10 @@ export class HuiStackCardEditor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cards = [...this._config.cards];
|
const cards = [...this._config.cards];
|
||||||
cards[this._selectedCard] = ev.detail.config as LovelaceCardConfig;
|
const key = this._getKey(cards[this._selectedCard]);
|
||||||
|
const newCard = ev.detail.config as LovelaceCardConfig;
|
||||||
|
cards[this._selectedCard] = newCard;
|
||||||
|
this._keys.set(newCard, key);
|
||||||
this._config = { ...this._config, cards };
|
this._config = { ...this._config, cards };
|
||||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||||
fireEvent(this, "config-changed", { config: this._config });
|
fireEvent(this, "config-changed", { config: this._config });
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
assert,
|
assert,
|
||||||
assign,
|
assign,
|
||||||
boolean,
|
boolean,
|
||||||
|
enums,
|
||||||
object,
|
object,
|
||||||
optional,
|
optional,
|
||||||
string,
|
string,
|
||||||
@ -15,6 +16,7 @@ import {
|
|||||||
} from "superstruct";
|
} from "superstruct";
|
||||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||||
import { fireEvent } 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-expansion-panel";
|
||||||
import "../../../../components/ha-form/ha-form";
|
import "../../../../components/ha-form/ha-form";
|
||||||
import type {
|
import type {
|
||||||
@ -54,6 +56,7 @@ const cardConfigStruct = assign(
|
|||||||
icon_hold_action: optional(actionConfigStruct),
|
icon_hold_action: optional(actionConfigStruct),
|
||||||
icon_double_tap_action: optional(actionConfigStruct),
|
icon_double_tap_action: optional(actionConfigStruct),
|
||||||
features: optional(array(any())),
|
features: optional(array(any())),
|
||||||
|
features_position: optional(enums(["bottom", "inline"])),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -109,8 +112,10 @@ export class HuiTileCardEditor
|
|||||||
|
|
||||||
private _schema = memoizeOne(
|
private _schema = memoizeOne(
|
||||||
(
|
(
|
||||||
|
localize: LocalizeFunc,
|
||||||
entityId: string | undefined,
|
entityId: string | undefined,
|
||||||
hideState: boolean,
|
hideState: boolean,
|
||||||
|
vertical: boolean,
|
||||||
displayActions: AdvancedActions[] = []
|
displayActions: AdvancedActions[] = []
|
||||||
) =>
|
) =>
|
||||||
[
|
[
|
||||||
@ -148,12 +153,6 @@ export class HuiTileCardEditor
|
|||||||
boolean: {},
|
boolean: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "vertical",
|
|
||||||
selector: {
|
|
||||||
boolean: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "hide_state",
|
name: "hide_state",
|
||||||
selector: {
|
selector: {
|
||||||
@ -175,6 +174,43 @@ export class HuiTileCardEditor
|
|||||||
},
|
},
|
||||||
] as const satisfies readonly HaFormSchema[])
|
] 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",
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -223,12 +259,22 @@ export class HuiTileCardEditor
|
|||||||
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
|
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
|
||||||
|
|
||||||
const schema = this._schema(
|
const schema = this._schema(
|
||||||
|
this.hass.localize,
|
||||||
entityId,
|
entityId,
|
||||||
this._config!.hide_state ?? false,
|
this._config.hide_state ?? false,
|
||||||
|
this._config.vertical ?? false,
|
||||||
this._displayActions
|
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`
|
return html`
|
||||||
<ha-form
|
<ha-form
|
||||||
@ -280,6 +326,12 @@ export class HuiTileCardEditor
|
|||||||
delete config.state_content;
|
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 });
|
fireEvent(this, "config-changed", { config });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,11 +389,11 @@ export class HuiTileCardEditor
|
|||||||
case "icon_hold_action":
|
case "icon_hold_action":
|
||||||
case "icon_double_tap_action":
|
case "icon_double_tap_action":
|
||||||
case "show_entity_picture":
|
case "show_entity_picture":
|
||||||
case "vertical":
|
|
||||||
case "hide_state":
|
case "hide_state":
|
||||||
case "state_content":
|
case "state_content":
|
||||||
|
case "content_layout":
|
||||||
case "appearance":
|
case "appearance":
|
||||||
case "interactions":
|
case "features_position":
|
||||||
return this.hass!.localize(
|
return this.hass!.localize(
|
||||||
`ui.panel.lovelace.editor.card.tile.${schema.name}`
|
`ui.panel.lovelace.editor.card.tile.${schema.name}`
|
||||||
);
|
);
|
||||||
@ -377,6 +429,14 @@ export class HuiTileCardEditor
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
.info {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.features-form {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
import { createSectionElement } from "../create-element/create-section-element";
|
import { createSectionElement } from "../create-element/create-section-element";
|
||||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||||
|
import { replaceCard } from "../editor/config-util";
|
||||||
import { performDeleteCard } from "../editor/delete-card";
|
import { performDeleteCard } from "../editor/delete-card";
|
||||||
import { parseLovelaceCardPath } from "../editor/lovelace-path";
|
import { parseLovelaceCardPath } from "../editor/lovelace-path";
|
||||||
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
|
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
|
||||||
@ -253,11 +254,23 @@ export class HuiSection extends ReactiveElement {
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (!this.lovelace) return;
|
if (!this.lovelace) return;
|
||||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||||
|
const sectionConfig = this.config;
|
||||||
|
if (isStrategySection(sectionConfig)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cardConfig = sectionConfig.cards![cardIndex];
|
||||||
showEditCardDialog(this, {
|
showEditCardDialog(this, {
|
||||||
lovelaceConfig: this.lovelace.config,
|
lovelaceConfig: this.lovelace.config,
|
||||||
saveConfig: this.lovelace.saveConfig,
|
saveCardConfig: async (newCardConfig) => {
|
||||||
path: [this.viewIndex, this.index],
|
const newConfig = replaceCard(
|
||||||
cardIndex,
|
this.lovelace!.config,
|
||||||
|
[this.viewIndex, this.index, cardIndex],
|
||||||
|
newCardConfig
|
||||||
|
);
|
||||||
|
await this.lovelace!.saveConfig(newConfig);
|
||||||
|
},
|
||||||
|
sectionConfig,
|
||||||
|
cardConfig,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||||
|
@ -21,6 +21,7 @@ import { showCreateBadgeDialog } from "../editor/badge-editor/show-create-badge-
|
|||||||
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
|
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
|
||||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||||
|
import { replaceCard } from "../editor/config-util";
|
||||||
import {
|
import {
|
||||||
type DeleteBadgeParams,
|
type DeleteBadgeParams,
|
||||||
performDeleteBadge,
|
performDeleteBadge,
|
||||||
@ -270,11 +271,22 @@ export class HUIView extends ReactiveElement {
|
|||||||
});
|
});
|
||||||
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||||
|
const viewConfig = this.lovelace!.config.views[this.index];
|
||||||
|
if (isStrategyView(viewConfig)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cardConfig = viewConfig.cards![cardIndex];
|
||||||
showEditCardDialog(this, {
|
showEditCardDialog(this, {
|
||||||
lovelaceConfig: this.lovelace.config,
|
lovelaceConfig: this.lovelace.config,
|
||||||
saveConfig: this.lovelace.saveConfig,
|
saveCardConfig: async (newCardConfig) => {
|
||||||
path: [this.index],
|
const newConfig = replaceCard(
|
||||||
cardIndex,
|
this.lovelace!.config,
|
||||||
|
[this.index, cardIndex],
|
||||||
|
newCardConfig
|
||||||
|
);
|
||||||
|
await this.lovelace.saveConfig(newConfig);
|
||||||
|
},
|
||||||
|
cardConfig,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||||
|
@ -47,7 +47,12 @@ export class HaStateControlAlarmControlPanelModes extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _setMode(mode: AlarmMode) {
|
private async _setMode(mode: AlarmMode) {
|
||||||
setProtectedAlarmControlPanelMode(this, this.hass!, this.stateObj!, mode);
|
await setProtectedAlarmControlPanelMode(
|
||||||
|
this,
|
||||||
|
this.hass!,
|
||||||
|
this.stateObj!,
|
||||||
|
mode
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _valueChanged(ev: CustomEvent) {
|
private async _valueChanged(ev: CustomEvent) {
|
||||||
|
@ -286,7 +286,10 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
|||||||
clearInterval(this.__backendPingInterval);
|
clearInterval(this.__backendPingInterval);
|
||||||
this.__backendPingInterval = setInterval(() => {
|
this.__backendPingInterval = setInterval(() => {
|
||||||
if (this.hass?.connected) {
|
if (this.hass?.connected) {
|
||||||
promiseTimeout(5000, this.hass?.connection.ping()).catch(() => {
|
// If the backend is busy, or the connection is latent,
|
||||||
|
// it can take more than 10 seconds for the ping to return.
|
||||||
|
// We give it a 15 second timeout to be safe.
|
||||||
|
promiseTimeout(15000, this.hass?.connection.ping()).catch(() => {
|
||||||
if (!this.hass?.connected) {
|
if (!this.hass?.connected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -296,7 +299,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
|||||||
this.hass?.connection.reconnect(true);
|
this.hass?.connection.reconnect(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected hassReconnected() {
|
protected hassReconnected() {
|
||||||
|
@ -370,7 +370,10 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"optional": "optional",
|
"optional": "optional",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"dont_save": "Don't save"
|
"dont_save": "Don't save",
|
||||||
|
"copy": "Copy",
|
||||||
|
"show": "Show",
|
||||||
|
"hide": "Hide"
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"selectors": {
|
"selectors": {
|
||||||
@ -2469,7 +2472,7 @@
|
|||||||
},
|
},
|
||||||
"retention": "Retention",
|
"retention": "Retention",
|
||||||
"custom_retention": "Custom 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_description": "Based on the maximum number of backups or how many days they should be kept.",
|
||||||
"retention_presets": {
|
"retention_presets": {
|
||||||
"copies_3": "3 backups",
|
"copies_3": "3 backups",
|
||||||
@ -2512,6 +2515,7 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"upload_backup": "Upload backup"
|
"upload_backup": "Upload backup"
|
||||||
},
|
},
|
||||||
|
"agent_error": "Error in location {name}",
|
||||||
"new_backup": "Backup now",
|
"new_backup": "Backup now",
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"title": "Set up backups",
|
"title": "Set up backups",
|
||||||
@ -2650,7 +2654,9 @@
|
|||||||
"title": "Locations",
|
"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.",
|
"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": "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": {
|
"encryption_key": {
|
||||||
"title": "Encryption key",
|
"title": "Encryption key",
|
||||||
@ -4724,6 +4730,23 @@
|
|||||||
"fingerprint": "Certificate fingerprint:",
|
"fingerprint": "Certificate fingerprint:",
|
||||||
"close": "Close"
|
"close": "Close"
|
||||||
},
|
},
|
||||||
|
"dialog_already_connected": {
|
||||||
|
"heading": "Account linked to other Home Assistant",
|
||||||
|
"description": "We noticed that another instance is currently connected to your Home Assistant Cloud account. Your Home Assistant Cloud account can only be signed into one Home Assistant instance at a time. If you log in here, the other instance will be disconnected along with its Cloud services.",
|
||||||
|
"other_home_assistant": "Other Home Assistant",
|
||||||
|
"ip_address": "IP Address",
|
||||||
|
"connected_at": "Connected at",
|
||||||
|
"obfuscated_ip": {
|
||||||
|
"show": "Show IP address",
|
||||||
|
"hide": "Hide IP address"
|
||||||
|
},
|
||||||
|
"info_backups": {
|
||||||
|
"title": "Home Assistant Cloud backups",
|
||||||
|
"description": "Your Cloud backup may be overwritten if you proceed. We strongly recommend downloading your current backup from your Nabu Casa account page before continuing."
|
||||||
|
},
|
||||||
|
"close": "Close",
|
||||||
|
"login_here": "Log in here"
|
||||||
|
},
|
||||||
"dialog_cloudhook": {
|
"dialog_cloudhook": {
|
||||||
"webhook_for": "Webhook for {name}",
|
"webhook_for": "Webhook for {name}",
|
||||||
"managed_by_integration": "This webhook is managed by an integration and cannot be disabled.",
|
"managed_by_integration": "This webhook is managed by an integration and cannot be disabled.",
|
||||||
@ -6307,7 +6330,8 @@
|
|||||||
"description": "Home Assistant is starting, please wait…"
|
"description": "Home Assistant is starting, please wait…"
|
||||||
},
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"reset_focus": "Reset focus"
|
"reset_focus": "Reset focus",
|
||||||
|
"toggle_grouping": "Toggle grouping"
|
||||||
},
|
},
|
||||||
"energy": {
|
"energy": {
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
@ -6994,7 +7018,8 @@
|
|||||||
"suggested_cards": "Suggested cards",
|
"suggested_cards": "Suggested cards",
|
||||||
"other_cards": "Other cards",
|
"other_cards": "Other cards",
|
||||||
"custom_cards": "Custom cards",
|
"custom_cards": "Custom cards",
|
||||||
"features": "Features"
|
"features": "Features",
|
||||||
|
"actions": "Actions"
|
||||||
},
|
},
|
||||||
"heading": {
|
"heading": {
|
||||||
"name": "Heading",
|
"name": "Heading",
|
||||||
@ -7041,6 +7066,11 @@
|
|||||||
"markdown": {
|
"markdown": {
|
||||||
"name": "Markdown",
|
"name": "Markdown",
|
||||||
"content": "Content",
|
"content": "Content",
|
||||||
|
"style": "Style",
|
||||||
|
"style_options": {
|
||||||
|
"card": "Card",
|
||||||
|
"text-only": "Text only"
|
||||||
|
},
|
||||||
"description": "The Markdown card is used to render Markdown."
|
"description": "The Markdown card is used to render Markdown."
|
||||||
},
|
},
|
||||||
"media-control": {
|
"media-control": {
|
||||||
@ -7119,12 +7149,20 @@
|
|||||||
"icon_tap_action": "Icon tap behavior",
|
"icon_tap_action": "Icon tap behavior",
|
||||||
"icon_hold_action": "Icon hold behavior",
|
"icon_hold_action": "Icon hold behavior",
|
||||||
"icon_double_tap_action": "Icon double tap behavior",
|
"icon_double_tap_action": "Icon double tap behavior",
|
||||||
"interactions": "Interactions",
|
|
||||||
"appearance": "Appearance",
|
"appearance": "Appearance",
|
||||||
"show_entity_picture": "Show entity picture",
|
"show_entity_picture": "Show entity picture",
|
||||||
"vertical": "Vertical",
|
|
||||||
"hide_state": "Hide state",
|
"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": {
|
"vertical-stack": {
|
||||||
"name": "Vertical stack",
|
"name": "Vertical stack",
|
||||||
@ -7300,6 +7338,14 @@
|
|||||||
"customize_modes": "Customize preset modes",
|
"customize_modes": "Customize preset modes",
|
||||||
"preset_modes": "Preset modes"
|
"preset_modes": "Preset modes"
|
||||||
},
|
},
|
||||||
|
"counter-actions": {
|
||||||
|
"label": "Counter actions",
|
||||||
|
"actions": {
|
||||||
|
"increment": "Increment",
|
||||||
|
"decrement": "Decrement",
|
||||||
|
"reset": "Reset"
|
||||||
|
}
|
||||||
|
},
|
||||||
"fan-preset-modes": {
|
"fan-preset-modes": {
|
||||||
"label": "Fan preset modes",
|
"label": "Fan preset modes",
|
||||||
"style": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style%]",
|
"style": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style%]",
|
||||||
@ -7328,6 +7374,9 @@
|
|||||||
"options": "Options",
|
"options": "Options",
|
||||||
"customize_options": "Customize options"
|
"customize_options": "Customize options"
|
||||||
},
|
},
|
||||||
|
"toggle": {
|
||||||
|
"label": "Toggle"
|
||||||
|
},
|
||||||
"numeric-input": {
|
"numeric-input": {
|
||||||
"label": "Numeric input",
|
"label": "Numeric input",
|
||||||
"style": "Style",
|
"style": "Style",
|
||||||
|
@ -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("#ff0000")).toBe("#ff0000");
|
||||||
expect(theme2hex("unicorn")).toBe("unicorn");
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -63,4 +63,28 @@ describe("canToggleState", () => {
|
|||||||
};
|
};
|
||||||
assert.isFalse(canToggleState(hass, stateObj));
|
assert.isFalse(canToggleState(hass, stateObj));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Detects group with missing entity", () => {
|
||||||
|
const stateObj: any = {
|
||||||
|
entity_id: "group.bla",
|
||||||
|
state: "on",
|
||||||
|
attributes: {
|
||||||
|
entity_id: ["light.non_existing"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.isFalse(canToggleState(hass, stateObj));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Detects group with off state", () => {
|
||||||
|
const stateObj: any = {
|
||||||
|
entity_id: "group.bla",
|
||||||
|
state: "off",
|
||||||
|
attributes: {
|
||||||
|
entity_id: ["light.test"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.isTrue(canToggleState(hass, stateObj));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
371
test/common/entity/compute_attribute_display.test.ts
Normal file
371
test/common/entity/compute_attribute_display.test.ts
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
import type {
|
||||||
|
HassConfig,
|
||||||
|
HassEntity,
|
||||||
|
HassEntityBase,
|
||||||
|
} from "home-assistant-js-websocket";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
computeAttributeValueDisplay,
|
||||||
|
computeAttributeNameDisplay,
|
||||||
|
} from "../../../src/common/entity/compute_attribute_display";
|
||||||
|
import type { FrontendLocaleData } from "../../../src/data/translation";
|
||||||
|
import type { HomeAssistant } from "../../../src/types";
|
||||||
|
|
||||||
|
export const localizeMock = (key: string) => {
|
||||||
|
const translations = {
|
||||||
|
"state.default.unknown": "Unknown",
|
||||||
|
"component.test_platform.entity.sensor.test_translation_key.state_attributes.attribute.state.42":
|
||||||
|
"42",
|
||||||
|
"component.test_platform.entity.sensor.test_translation_key.state_attributes.attribute.state.attributeValue":
|
||||||
|
"Localized Attribute Name",
|
||||||
|
"component.media_player.entity_component.media_player.state_attributes.attribute.state.attributeValue":
|
||||||
|
"Localized Media Player Attribute Name",
|
||||||
|
"component.media_player.entity_component._.state_attributes.attribute.state.attributeValue":
|
||||||
|
"Media Player Attribute Name",
|
||||||
|
};
|
||||||
|
return translations[key] || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stateObjMock = {
|
||||||
|
entity_id: "sensor.test",
|
||||||
|
attributes: {
|
||||||
|
device_class: "temperature",
|
||||||
|
},
|
||||||
|
} as HassEntityBase;
|
||||||
|
|
||||||
|
export const localeMock = {
|
||||||
|
language: "en",
|
||||||
|
} as FrontendLocaleData;
|
||||||
|
|
||||||
|
export const configMock = {
|
||||||
|
unit_system: {
|
||||||
|
temperature: "°C",
|
||||||
|
},
|
||||||
|
} as HassConfig;
|
||||||
|
|
||||||
|
export const entitiesMock = {
|
||||||
|
"sensor.test": {
|
||||||
|
platform: "test_platform",
|
||||||
|
translation_key: "test_translation_key",
|
||||||
|
},
|
||||||
|
"media_player.test": {
|
||||||
|
platform: "media_player",
|
||||||
|
},
|
||||||
|
} as unknown as HomeAssistant["entities"];
|
||||||
|
|
||||||
|
describe("computeAttributeValueDisplay", () => {
|
||||||
|
it("should return unknown state for null value", () => {
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObjMock,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"attribute",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
expect(result).toBe("Unknown");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return formatted number for numeric value", () => {
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObjMock,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"attribute",
|
||||||
|
42
|
||||||
|
);
|
||||||
|
expect(result).toBe("42");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return number from formatter", () => {
|
||||||
|
const stateObj = {
|
||||||
|
entity_id: "media_player.test",
|
||||||
|
attributes: {
|
||||||
|
device_class: "media_player",
|
||||||
|
volume_level: 0.42,
|
||||||
|
},
|
||||||
|
} as unknown as HassEntityBase;
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObj,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"volume_level"
|
||||||
|
);
|
||||||
|
expect(result).toBe("42%");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return formatted date for date string", () => {
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObjMock,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"attribute",
|
||||||
|
"2023-10-10"
|
||||||
|
);
|
||||||
|
expect(result).toBe("October 10, 2023");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return formatted datetime for timestamp", () => {
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObjMock,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"attribute",
|
||||||
|
"2023-10-10T10:10:10"
|
||||||
|
);
|
||||||
|
expect(result).toBe("October 10, 2023 at 10:10:10");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return JSON string for object value", () => {
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObjMock,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"attribute",
|
||||||
|
{ key: "value" }
|
||||||
|
);
|
||||||
|
expect(result).toBe('{"key":"value"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return concatenated values for array", () => {
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObjMock,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"attribute",
|
||||||
|
[1, 2, 3]
|
||||||
|
);
|
||||||
|
expect(result).toBe("1, 2, 3");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set special unit for weather domain", () => {
|
||||||
|
const stateObj = {
|
||||||
|
entity_id: "weather.test",
|
||||||
|
attributes: {
|
||||||
|
temperature: 42,
|
||||||
|
},
|
||||||
|
} as unknown as HassEntityBase;
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObj,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"temperature"
|
||||||
|
);
|
||||||
|
expect(result).toBe("42 °C");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set temperature unit for temperature attribute", () => {
|
||||||
|
const stateObj = {
|
||||||
|
entity_id: "sensor.test",
|
||||||
|
attributes: {
|
||||||
|
temperature: 42,
|
||||||
|
},
|
||||||
|
} as unknown as HassEntityBase;
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObj,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"temperature"
|
||||||
|
);
|
||||||
|
expect(result).toBe("42 °C");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return translation from translation key", () => {
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObjMock,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"attribute",
|
||||||
|
"attributeValue"
|
||||||
|
);
|
||||||
|
expect(result).toBe("Localized Attribute Name");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return device class translation", () => {
|
||||||
|
const stateObj = {
|
||||||
|
entity_id: "media_player.test",
|
||||||
|
attributes: {
|
||||||
|
device_class: "media_player",
|
||||||
|
volume_level: 0.42,
|
||||||
|
},
|
||||||
|
} as unknown as HassEntityBase;
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObj,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"attribute",
|
||||||
|
"attributeValue"
|
||||||
|
);
|
||||||
|
expect(result).toBe("Localized Media Player Attribute Name");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return attribute value translation", () => {
|
||||||
|
const stateObj = {
|
||||||
|
entity_id: "media_player.test",
|
||||||
|
attributes: {
|
||||||
|
volume_level: 0.42,
|
||||||
|
},
|
||||||
|
} as unknown as HassEntityBase;
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObj,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"attribute",
|
||||||
|
"attributeValue"
|
||||||
|
);
|
||||||
|
expect(result).toBe("Media Player Attribute Name");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return attribute value", () => {
|
||||||
|
const stateObj = {
|
||||||
|
entity_id: "media_player.test",
|
||||||
|
attributes: {
|
||||||
|
volume_level: 0.42,
|
||||||
|
},
|
||||||
|
} as unknown as HassEntityBase;
|
||||||
|
const result = computeAttributeValueDisplay(
|
||||||
|
localizeMock,
|
||||||
|
stateObj,
|
||||||
|
localeMock,
|
||||||
|
configMock,
|
||||||
|
entitiesMock,
|
||||||
|
"attribute",
|
||||||
|
"attributeValue2"
|
||||||
|
);
|
||||||
|
expect(result).toBe("attributeValue2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("computeAttributeNameDisplay", () => {
|
||||||
|
it("should return localized name for attribute", () => {
|
||||||
|
const localize = (key: string) => {
|
||||||
|
if (
|
||||||
|
key ===
|
||||||
|
"component.light.entity.light.entity_translation_key.state_attributes.updated_at.name"
|
||||||
|
) {
|
||||||
|
return "Updated at";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateObj = {
|
||||||
|
entity_id: "light.test",
|
||||||
|
attributes: {
|
||||||
|
device_class: "light",
|
||||||
|
},
|
||||||
|
} as HassEntity;
|
||||||
|
|
||||||
|
const entities = {
|
||||||
|
"light.test": {
|
||||||
|
translation_key: "entity_translation_key",
|
||||||
|
platform: "light",
|
||||||
|
},
|
||||||
|
} as unknown as HomeAssistant["entities"];
|
||||||
|
|
||||||
|
const result = computeAttributeNameDisplay(
|
||||||
|
localize,
|
||||||
|
stateObj,
|
||||||
|
entities,
|
||||||
|
"updated_at"
|
||||||
|
);
|
||||||
|
expect(result).toBe("Updated at");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return device class translation", () => {
|
||||||
|
const localize = (key: string) => {
|
||||||
|
if (
|
||||||
|
key ===
|
||||||
|
"component.light.entity_component.light.state_attributes.brightness.name"
|
||||||
|
) {
|
||||||
|
return "Brightness";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateObj = {
|
||||||
|
entity_id: "light.test",
|
||||||
|
attributes: {
|
||||||
|
device_class: "light",
|
||||||
|
},
|
||||||
|
} as HassEntity;
|
||||||
|
|
||||||
|
const entities = {} as unknown as HomeAssistant["entities"];
|
||||||
|
|
||||||
|
const result = computeAttributeNameDisplay(
|
||||||
|
localize,
|
||||||
|
stateObj,
|
||||||
|
entities,
|
||||||
|
"brightness"
|
||||||
|
);
|
||||||
|
expect(result).toBe("Brightness");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return default attribute name", () => {
|
||||||
|
const localize = (key: string) => {
|
||||||
|
if (
|
||||||
|
key ===
|
||||||
|
"component.light.entity_component._.state_attributes.brightness.name"
|
||||||
|
) {
|
||||||
|
return "Brightness";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateObj = {
|
||||||
|
entity_id: "light.test",
|
||||||
|
attributes: {},
|
||||||
|
} as HassEntity;
|
||||||
|
|
||||||
|
const entities = {} as unknown as HomeAssistant["entities"];
|
||||||
|
|
||||||
|
const result = computeAttributeNameDisplay(
|
||||||
|
localize,
|
||||||
|
stateObj,
|
||||||
|
entities,
|
||||||
|
"brightness"
|
||||||
|
);
|
||||||
|
expect(result).toBe("Brightness");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return capitalized attribute name", () => {
|
||||||
|
const localize = () => "";
|
||||||
|
|
||||||
|
const stateObj = {
|
||||||
|
entity_id: "light.test",
|
||||||
|
attributes: {},
|
||||||
|
} as HassEntity;
|
||||||
|
|
||||||
|
const entities = {} as unknown as HomeAssistant["entities"];
|
||||||
|
|
||||||
|
const result = computeAttributeNameDisplay(
|
||||||
|
localize,
|
||||||
|
stateObj,
|
||||||
|
entities,
|
||||||
|
"brightness__ip_id_mac_gps_GPS"
|
||||||
|
);
|
||||||
|
expect(result).toBe("Brightness IP ID MAC GPS GPS");
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,9 @@
|
|||||||
import { assert, describe, it, beforeEach } from "vitest";
|
import type { HassConfig } from "home-assistant-js-websocket";
|
||||||
import { computeStateDisplay } from "../../../src/common/entity/compute_state_display";
|
import { assert, describe, it, beforeEach, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
computeStateDisplay,
|
||||||
|
computeStateDisplayFromEntityAttributes,
|
||||||
|
} from "../../../src/common/entity/compute_state_display";
|
||||||
import { UNKNOWN } from "../../../src/data/entity";
|
import { UNKNOWN } from "../../../src/data/entity";
|
||||||
import type { FrontendLocaleData } from "../../../src/data/translation";
|
import type { FrontendLocaleData } from "../../../src/data/translation";
|
||||||
import {
|
import {
|
||||||
@ -10,6 +14,7 @@ import {
|
|||||||
TimeZone,
|
TimeZone,
|
||||||
} from "../../../src/data/translation";
|
} from "../../../src/data/translation";
|
||||||
import { demoConfig } from "../../../src/fake_data/demo_config";
|
import { demoConfig } from "../../../src/fake_data/demo_config";
|
||||||
|
import type { EntityRegistryDisplayEntry } from "../../../src/data/entity_registry";
|
||||||
|
|
||||||
let localeData: FrontendLocaleData;
|
let localeData: FrontendLocaleData;
|
||||||
|
|
||||||
@ -617,3 +622,85 @@ describe("computeStateDisplay", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("computeStateDisplayFromEntityAttributes with numeric device classes", () => {
|
||||||
|
it("Should format duration sensor", () => {
|
||||||
|
const result = computeStateDisplayFromEntityAttributes(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
(() => {}) as any,
|
||||||
|
{
|
||||||
|
language: "en",
|
||||||
|
} as FrontendLocaleData,
|
||||||
|
[],
|
||||||
|
{} as HassConfig,
|
||||||
|
{
|
||||||
|
display_precision: 2,
|
||||||
|
} as EntityRegistryDisplayEntry,
|
||||||
|
"number.test",
|
||||||
|
{
|
||||||
|
device_class: "duration",
|
||||||
|
unit_of_measurement: "min",
|
||||||
|
},
|
||||||
|
"12"
|
||||||
|
);
|
||||||
|
expect(result).toBe("12.00 min");
|
||||||
|
});
|
||||||
|
it("Should format duration sensor with seconds", () => {
|
||||||
|
const result = computeStateDisplayFromEntityAttributes(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
(() => {}) as any,
|
||||||
|
{
|
||||||
|
language: "en",
|
||||||
|
} as FrontendLocaleData,
|
||||||
|
[],
|
||||||
|
{} as HassConfig,
|
||||||
|
undefined,
|
||||||
|
"number.test",
|
||||||
|
{
|
||||||
|
device_class: "duration",
|
||||||
|
unit_of_measurement: "s",
|
||||||
|
},
|
||||||
|
"12"
|
||||||
|
);
|
||||||
|
expect(result).toBe("12 s");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should format monetary device_class", () => {
|
||||||
|
const result = computeStateDisplayFromEntityAttributes(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
(() => {}) as any,
|
||||||
|
{
|
||||||
|
language: "en",
|
||||||
|
} as FrontendLocaleData,
|
||||||
|
[],
|
||||||
|
{} as HassConfig,
|
||||||
|
undefined,
|
||||||
|
"number.test",
|
||||||
|
{
|
||||||
|
device_class: "monetary",
|
||||||
|
unit_of_measurement: "$",
|
||||||
|
},
|
||||||
|
"12"
|
||||||
|
);
|
||||||
|
expect(result).toBe("12 $");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("computeStateDisplayFromEntityAttributes datetime device calss", () => {
|
||||||
|
it("Should format datetime sensor", () => {
|
||||||
|
const result = computeStateDisplayFromEntityAttributes(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
(() => {}) as any,
|
||||||
|
{
|
||||||
|
language: "en",
|
||||||
|
} as FrontendLocaleData,
|
||||||
|
[],
|
||||||
|
{} as HassConfig,
|
||||||
|
undefined,
|
||||||
|
"button.test",
|
||||||
|
{},
|
||||||
|
"2020-01-01T12:00:00+00:00"
|
||||||
|
);
|
||||||
|
expect(result).toBe("January 1, 2020 at 12:00");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
plugins: [tsconfigPaths()],
|
||||||
test: {
|
test: {
|
||||||
environment: "jsdom", // to run in browser-like environment
|
environment: "jsdom", // to run in browser-like environment
|
||||||
env: {
|
env: {
|
||||||
|
@ -49,6 +49,36 @@
|
|||||||
"./node_modules/@lrnwebcomponents/simple-tooltip/custom-elements.json"
|
"./node_modules/@lrnwebcomponents/simple-tooltip/custom-elements.json"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"lit/static-html": ["./node_modules/lit/static-html.js"],
|
||||||
|
"lit/decorators": ["./node_modules/lit/decorators.js"],
|
||||||
|
"lit/directive": ["./node_modules/lit/directive.js"],
|
||||||
|
"lit/directives/until": ["./node_modules/lit/directives/until.js"],
|
||||||
|
"lit/directives/class-map": [
|
||||||
|
"./node_modules/lit/directives/class-map.js"
|
||||||
|
],
|
||||||
|
"lit/directives/style-map": [
|
||||||
|
"./node_modules/lit/directives/style-map.js"
|
||||||
|
],
|
||||||
|
"lit/directives/if-defined": [
|
||||||
|
"./node_modules/lit/directives/if-defined.js"
|
||||||
|
],
|
||||||
|
"lit/directives/guard": ["./node_modules/lit/directives/guard.js"],
|
||||||
|
"lit/directives/cache": ["./node_modules/lit/directives/cache.js"],
|
||||||
|
"lit/directives/repeat": ["./node_modules/lit/directives/repeat.js"],
|
||||||
|
"lit/directives/live": ["./node_modules/lit/directives/live.js"],
|
||||||
|
"lit/directives/keyed": ["./node_modules/lit/directives/keyed.js"],
|
||||||
|
"lit/polyfill-support": ["./node_modules/lit/polyfill-support.js"],
|
||||||
|
"@lit-labs/virtualizer/layouts/grid": [
|
||||||
|
"./node_modules/@lit-labs/virtualizer/layouts/grid.js"
|
||||||
|
],
|
||||||
|
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver": [
|
||||||
|
"./node_modules/@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js"
|
||||||
|
],
|
||||||
|
"@lit-labs/observers/resize-controller": [
|
||||||
|
"./node_modules/@lit-labs/observers/resize-controller.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user