mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-02 21:22:11 +00:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 928ac33f94 | |||
| 4d75ea5198 | |||
| ba3a63f856 | |||
| fd25d38be6 | |||
| ac22374a00 | |||
| de529cc26b | |||
| 126db3e8df | |||
| ed6fd59968 | |||
| 962e941ec9 | |||
| 31495b2de9 | |||
| f71dcaeac1 | |||
| 26b1bfbe20 | |||
| c6026507a5 | |||
| 53b5b43c33 | |||
| cb31afa432 | |||
| d5deff34ea | |||
| 1aed9f8b1f | |||
| 34ec052568 | |||
| 65f11ebc3f | |||
| 36cadeef40 | |||
| 1ed1b7bfda | |||
| e7d9b5348e | |||
| fa2c0278cb | |||
| fdd0636d9a | |||
| 39d1173a98 | |||
| 907687c16d | |||
| 3e17779550 | |||
| c91802d552 | |||
| 40f3d68f8b | |||
| 3aa2352f03 | |||
| cc98634fad | |||
| d61829e4d4 | |||
| 95ce9cb8c1 | |||
| 846a20dd13 | |||
| 36031c7365 | |||
| f99d95232a | |||
| 5b4c08ad04 | |||
| b0b2d84287 | |||
| 0a43c29fea | |||
| 61a8df1fa9 | |||
| ba3b335f47 | |||
| 5449f31162 | |||
| 1a992aa5f7 | |||
| 6303affe17 | |||
| fc0ac85223 | |||
| 2efc1a658f | |||
| 371642ef3f | |||
| aff9ec4345 | |||
| 80e37e7136 | |||
| 54a234debd | |||
| eeb0fb3e4d | |||
| d5dc40fa1f | |||
| df12232e93 | |||
| 1a2e63a5ba | |||
| 581ba23f4e | |||
| 5dda82a8ac | |||
| 5a296fadec | |||
| 5dc99a3dbd | |||
| 2f36c64a21 | |||
| 59cfc82e7a | |||
| edc36b28a6 | |||
| 7b58162f81 | |||
| 9e9f247c79 | |||
| b95fce5f24 | |||
| f6abbe80a2 | |||
| 71301ef5be | |||
| 4af618ac6f | |||
| 5a21ef67cd | |||
| 251b9a1b94 | |||
| cadaa47bf0 | |||
| 7ec864dc6d | |||
| 680ceb73e9 | |||
| ab31771055 | |||
| 07b9a6e287 | |||
| be2c90cd1c | |||
| 2af4ff7c8f | |||
| 8139b60248 | |||
| 386372ad00 | |||
| 9151b200a1 | |||
| f449c6c1c1 | |||
| d1eb3fd162 | |||
| f7e92b484a | |||
| 9fab7bafdb | |||
| 0dabb02007 | |||
| 5b73e86786 | |||
| 144d7c5c3f | |||
| 8b396dc640 | |||
| 9bf48d30ab | |||
| 35fee46f5b | |||
| 9ac6636029 | |||
| 136462114d | |||
| cf542197e0 | |||
| 5c2627624a | |||
| 698ded9d85 | |||
| 9a7fb96873 | |||
| 204c5b5e14 | |||
| 8ea3acfa98 | |||
| 306739773e | |||
| 8b3fa3adac | |||
| 37a1d59a24 | |||
| 6812884e00 | |||
| bf7ef1f7ae | |||
| fe57f601ba | |||
| c89d478440 | |||
| fa27d26e5f | |||
| 18f411ef53 | |||
| 24826e92f0 | |||
| ea9d369d88 | |||
| a9b026d0ef | |||
| 35339906ec | |||
| ce23f716cc | |||
| aaf8fa199f | |||
| fba430d507 | |||
| 59361cbd38 | |||
| b558117d8c | |||
| a7c8347751 | |||
| 31ca9c849a | |||
| 6252d7e8f5 | |||
| f42986adf6 | |||
| 9e70ea3723 | |||
| de3b7bf513 | |||
| 2c5f491c9e | |||
| 1ef13c5100 | |||
| c166335aca | |||
| c64ec21eca | |||
| 8d62056f4a | |||
| 62e73608b6 | |||
| aa66d8891c | |||
| 494a96c635 | |||
| 36d77f54ce | |||
| 12fec9f580 | |||
| 5f1f55448a | |||
| 837e345ecf | |||
| 0929d7d18a | |||
| 70991d3c1e | |||
| 82e5bd62a1 | |||
| b8adf4e866 | |||
| 111be984e0 | |||
| 78a2cbb532 | |||
| 34b09b140b | |||
| f173f901c5 | |||
| ebb6ac8d8b | |||
| abe214a33a | |||
| 248332ae27 | |||
| 82fc2fccdc | |||
| c8f30a7ee4 | |||
| 77f48d91cd | |||
| caa707a7b1 | |||
| 0bed0fa37e | |||
| 5b6309d984 | |||
| 264818bc70 | |||
| d664ab6836 | |||
| a6c4184054 | |||
| cb6985eb7c | |||
| d466ab63bd | |||
| 1132cdb364 | |||
| 0f9d48a03d | |||
| 7e085d9b08 | |||
| 1a62c7296c | |||
| be1921229c | |||
| 640558ad35 | |||
| 99636c9719 |
@@ -30,7 +30,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
@@ -41,14 +41,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -62,4 +62,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@f6f29a7ee3fa0eccadf3620a7b9ee00ab54ec03b # master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -42,7 +42,7 @@ class HcDemo extends HassElement {
|
||||
this._updateHass(hassUpdate),
|
||||
};
|
||||
|
||||
const hass = (this.hass = provideHass(this, initial));
|
||||
const hass = provideHass(this, initial, true);
|
||||
|
||||
mockHistory(hass);
|
||||
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
this._updateHass(hassUpdate),
|
||||
};
|
||||
|
||||
const hass = (this.hass = provideHass(this, initial));
|
||||
const hass = provideHass(this, initial, true);
|
||||
const localizePromise =
|
||||
// @ts-ignore
|
||||
this._loadFragmentTranslations(hass.language, "page-demo").then(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import type { LovelaceInfo } from "../../../src/data/lovelace/resource";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import {
|
||||
selectedDemoConfig,
|
||||
@@ -27,6 +28,9 @@ export const mockLovelace = (
|
||||
);
|
||||
});
|
||||
|
||||
hass.mockWS("lovelace/info", () =>
|
||||
Promise.resolve({ resource_mode: "storage" } as LovelaceInfo)
|
||||
);
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
|
||||
|
||||
@@ -15,7 +15,6 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
const generateMeanStatistics = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
// eslint-disable-next-line default-param-last
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
maxDiff: number
|
||||
): StatisticValue[] => {
|
||||
@@ -49,7 +48,6 @@ const generateMeanStatistics = (
|
||||
const generateSumStatistics = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
// eslint-disable-next-line default-param-last
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number
|
||||
@@ -86,7 +84,6 @@ const generateSumStatistics = (
|
||||
const generateCurvedStatistics = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
// eslint-disable-next-line default-param-last
|
||||
_period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number,
|
||||
|
||||
+52
-54
@@ -1,58 +1,56 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockSensor = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("sensor/numeric_device_classes", () => [
|
||||
{
|
||||
numeric_device_classes: [
|
||||
"volume_storage",
|
||||
"gas",
|
||||
"data_size",
|
||||
"irradiance",
|
||||
"wind_speed",
|
||||
"volatile_organic_compounds",
|
||||
"volatile_organic_compounds_parts",
|
||||
"voltage",
|
||||
"frequency",
|
||||
"precipitation_intensity",
|
||||
"volume",
|
||||
"precipitation",
|
||||
"battery",
|
||||
"nitrogen_dioxide",
|
||||
"speed",
|
||||
"signal_strength",
|
||||
"pm1",
|
||||
"nitrous_oxide",
|
||||
"atmospheric_pressure",
|
||||
"data_rate",
|
||||
"temperature",
|
||||
"power_factor",
|
||||
"aqi",
|
||||
"current",
|
||||
"volume_flow_rate",
|
||||
"humidity",
|
||||
"duration",
|
||||
"ozone",
|
||||
"distance",
|
||||
"pressure",
|
||||
"pm25",
|
||||
"weight",
|
||||
"energy",
|
||||
"carbon_monoxide",
|
||||
"apparent_power",
|
||||
"illuminance",
|
||||
"energy_storage",
|
||||
"moisture",
|
||||
"power",
|
||||
"water",
|
||||
"carbon_dioxide",
|
||||
"ph",
|
||||
"reactive_power",
|
||||
"monetary",
|
||||
"nitrogen_monoxide",
|
||||
"pm10",
|
||||
"sound_pressure",
|
||||
"sulphur_dioxide",
|
||||
],
|
||||
},
|
||||
]);
|
||||
hass.mockWS("sensor/numeric_device_classes", () => ({
|
||||
numeric_device_classes: [
|
||||
"volume_storage",
|
||||
"gas",
|
||||
"data_size",
|
||||
"irradiance",
|
||||
"wind_speed",
|
||||
"volatile_organic_compounds",
|
||||
"volatile_organic_compounds_parts",
|
||||
"voltage",
|
||||
"frequency",
|
||||
"precipitation_intensity",
|
||||
"volume",
|
||||
"precipitation",
|
||||
"battery",
|
||||
"nitrogen_dioxide",
|
||||
"speed",
|
||||
"signal_strength",
|
||||
"pm1",
|
||||
"nitrous_oxide",
|
||||
"atmospheric_pressure",
|
||||
"data_rate",
|
||||
"temperature",
|
||||
"power_factor",
|
||||
"aqi",
|
||||
"current",
|
||||
"volume_flow_rate",
|
||||
"humidity",
|
||||
"duration",
|
||||
"ozone",
|
||||
"distance",
|
||||
"pressure",
|
||||
"pm25",
|
||||
"weight",
|
||||
"energy",
|
||||
"carbon_monoxide",
|
||||
"apparent_power",
|
||||
"illuminance",
|
||||
"energy_storage",
|
||||
"moisture",
|
||||
"power",
|
||||
"water",
|
||||
"carbon_dioxide",
|
||||
"ph",
|
||||
"reactive_power",
|
||||
"monetary",
|
||||
"nitrogen_monoxide",
|
||||
"pm10",
|
||||
"sound_pressure",
|
||||
"sulphur_dioxide",
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
+53
-64
@@ -2,10 +2,7 @@
|
||||
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import globals from "globals";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import tseslint from "typescript-eslint";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import { configs as litConfigs } from "eslint-plugin-lit";
|
||||
@@ -14,35 +11,8 @@ import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
|
||||
import html from "@html-eslint/eslint-plugin";
|
||||
import importX from "eslint-plugin-import-x";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: _dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
// Load airbnb-base via FlatCompat for non-import rules only.
|
||||
// eslint-plugin-import is incompatible with ESLint 10 (uses removed APIs),
|
||||
// so we strip its plugin/rules/settings and use eslint-plugin-import-x instead.
|
||||
const airbnbConfigs = compat.extends("airbnb-base").map((config) => {
|
||||
const { plugins = {}, rules = {}, settings = {}, ...rest } = config;
|
||||
return {
|
||||
...rest,
|
||||
plugins: Object.fromEntries(
|
||||
Object.entries(plugins).filter(([key]) => key !== "import")
|
||||
),
|
||||
rules: Object.fromEntries(
|
||||
Object.entries(rules).filter(([key]) => !key.startsWith("import/"))
|
||||
),
|
||||
settings: Object.fromEntries(
|
||||
Object.entries(settings).filter(([key]) => !key.startsWith("import/"))
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
export default tseslint.config(
|
||||
...airbnbConfigs,
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
litConfigs["flat/all"],
|
||||
tseslint.configs.recommended,
|
||||
@@ -86,35 +56,59 @@ export default tseslint.config(
|
||||
},
|
||||
|
||||
rules: {
|
||||
"class-methods-use-this": "off",
|
||||
"new-cap": "off",
|
||||
"prefer-template": "off",
|
||||
"object-shorthand": "off",
|
||||
"func-names": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
strict: "off",
|
||||
"no-plusplus": "off",
|
||||
"no-bitwise": "error",
|
||||
"comma-dangle": "off",
|
||||
"vars-on-top": "off",
|
||||
"no-continue": "off",
|
||||
"no-param-reassign": "off",
|
||||
"no-multi-assign": "off",
|
||||
"no-console": "error",
|
||||
radix: "off",
|
||||
"no-alert": "off",
|
||||
"no-nested-ternary": "off",
|
||||
"prefer-destructuring": "off",
|
||||
"no-restricted-globals": [2, "event"],
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"object-curly-newline": "off",
|
||||
"default-case": "off",
|
||||
"wc/no-self-class": "off",
|
||||
"no-shadow": "off",
|
||||
"no-use-before-define": "off",
|
||||
"array-callback-return": ["error", { allowImplicit: true }],
|
||||
"block-scoped-var": "error",
|
||||
"consistent-return": "error",
|
||||
curly: ["error", "multi-line"],
|
||||
"default-case-last": "error",
|
||||
eqeqeq: ["error", "always", { null: "ignore" }],
|
||||
"guard-for-in": "error",
|
||||
"no-await-in-loop": "error",
|
||||
"no-caller": "error",
|
||||
"no-constructor-return": "error",
|
||||
"no-eval": "error",
|
||||
"no-extend-native": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-iterator": "error",
|
||||
"no-new-func": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-octal-escape": "error",
|
||||
"no-promise-executor-return": "error",
|
||||
"no-return-assign": ["error", "always"],
|
||||
"no-script-url": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
"no-template-curly-in-string": "error",
|
||||
"no-unreachable-loop": "error",
|
||||
|
||||
// import-x rules (migrated from eslint-plugin-import / airbnb-base)
|
||||
"no-else-return": ["error", { allowElseIf: false }],
|
||||
"no-lonely-if": "error",
|
||||
"no-unneeded-ternary": ["error", { defaultAssignment: false }],
|
||||
"no-useless-computed-key": "error",
|
||||
"no-useless-concat": "error",
|
||||
"no-useless-rename": "error",
|
||||
"no-useless-return": "error",
|
||||
"one-var": ["error", "never"],
|
||||
"operator-assignment": ["error", "always"],
|
||||
"prefer-arrow-callback": "error",
|
||||
"prefer-exponentiation-operator": "error",
|
||||
"prefer-object-spread": "error",
|
||||
"prefer-regex-literals": ["error", { disallowRedundantWrapping: true }],
|
||||
"symbol-description": "error",
|
||||
yoda: "error",
|
||||
|
||||
// TODO: Enable once violations are fixed (43 instances as of 2026-04)
|
||||
// "no-useless-assignment": "error",
|
||||
"no-useless-assignment": "error",
|
||||
|
||||
// Project rules
|
||||
"no-bitwise": "error",
|
||||
"no-console": "error",
|
||||
"no-restricted-globals": [2, "event"],
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"wc/no-self-class": "off",
|
||||
|
||||
// import-x rules
|
||||
"import-x/named": "off",
|
||||
"import-x/prefer-default-export": "off",
|
||||
"import-x/no-default-export": "off",
|
||||
@@ -146,13 +140,9 @@ export default tseslint.config(
|
||||
"import-x/no-relative-packages": "error",
|
||||
|
||||
// TypeScript rules
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
|
||||
"@typescript-eslint/naming-convention": [
|
||||
@@ -216,7 +206,6 @@ export default tseslint.config(
|
||||
"lit-a11y/role-has-required-aria-attrs": "error",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/no-import-type-side-effects": "error",
|
||||
camelcase: "off",
|
||||
"@typescript-eslint/no-dynamic-delete": "off",
|
||||
"@typescript-eslint/no-empty-object-type": [
|
||||
"error",
|
||||
|
||||
+79
-29
@@ -1,17 +1,22 @@
|
||||
import "@material/mwc-drawer";
|
||||
import "@material/mwc-top-app-bar-fixed";
|
||||
import { mdiMenu } from "@mdi/js";
|
||||
import { mdiMenu, mdiSwapHorizontal } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
|
||||
import { setDirectionStyles } from "../../src/common/util/compute_rtl";
|
||||
import "../../src/components/ha-button";
|
||||
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
|
||||
import "../../src/components/ha-icon-button";
|
||||
import "../../src/components/ha-svg-icon";
|
||||
import "../../src/managers/notification-manager";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import { PAGES, SIDEBAR } from "../build/import-pages";
|
||||
import "./components/page-description";
|
||||
|
||||
const RTL_STORAGE_KEY = "gallery-rtl";
|
||||
|
||||
const GITHUB_DEMO_URL =
|
||||
"https://github.com/home-assistant/frontend/blob/dev/gallery/src/pages/";
|
||||
|
||||
@@ -29,6 +34,8 @@ class HaGallery extends LitElement {
|
||||
document.location.hash.substring(1) ||
|
||||
`${SIDEBAR[0].category}/${SIDEBAR[0].pages![0]}`;
|
||||
|
||||
@state() private _rtl = localStorage.getItem(RTL_STORAGE_KEY) === "true";
|
||||
|
||||
@query("notification-manager")
|
||||
private _notifications!: HTMLElementTagNameMap["notification-manager"];
|
||||
|
||||
@@ -97,33 +104,43 @@ class HaGallery extends LitElement {
|
||||
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
|
||||
</div>
|
||||
<div class="page-footer">
|
||||
<div class="header">Help us to improve our documentation</div>
|
||||
<div class="secondary">
|
||||
Suggest an edit to this page, or provide/view feedback for this
|
||||
page.
|
||||
<div class="edit-docs">
|
||||
<div class="header">Help us to improve our documentation</div>
|
||||
<div class="secondary">
|
||||
Suggest an edit to this page, or provide/view feedback for this
|
||||
page.
|
||||
</div>
|
||||
<div>
|
||||
${PAGES[this._page].description ||
|
||||
Object.keys(PAGES[this._page].metadata).length > 0
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit text
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
${PAGES[this._page].demo
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit demo
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
${PAGES[this._page].description ||
|
||||
Object.keys(PAGES[this._page].metadata).length > 0
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit text
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
${PAGES[this._page].demo
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit demo
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
<div class="rtl-toggle">
|
||||
<ha-icon-button
|
||||
@click=${this._toggleRtl}
|
||||
.label=${this._rtl ? "Switch to LTR" : "Switch to RTL"}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,6 +155,8 @@ class HaGallery extends LitElement {
|
||||
firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._applyDirection();
|
||||
|
||||
this.addEventListener("show-notification", (ev) =>
|
||||
this._notifications.showDialog({ message: ev.detail.message })
|
||||
);
|
||||
@@ -164,6 +183,11 @@ class HaGallery extends LitElement {
|
||||
|
||||
updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("_rtl")) {
|
||||
this._applyDirection();
|
||||
}
|
||||
|
||||
if (!changedProps.has("_page")) {
|
||||
return;
|
||||
}
|
||||
@@ -186,6 +210,15 @@ class HaGallery extends LitElement {
|
||||
this._drawer.open = !this._drawer.open;
|
||||
}
|
||||
|
||||
private _toggleRtl() {
|
||||
this._rtl = !this._rtl;
|
||||
localStorage.setItem(RTL_STORAGE_KEY, String(this._rtl));
|
||||
}
|
||||
|
||||
private _applyDirection() {
|
||||
setDirectionStyles(this._rtl ? "rtl" : "ltr", this);
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
@@ -238,11 +271,16 @@ class HaGallery extends LitElement {
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
display: flex;
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.edit-docs {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
margin: 16px;
|
||||
padding: 16px;
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.page-footer div {
|
||||
@@ -266,6 +304,18 @@ class HaGallery extends LitElement {
|
||||
margin: 0 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.rtl-toggle {
|
||||
padding: var(--ha-space-4);
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
margin-top: 12px !important;
|
||||
}
|
||||
|
||||
.rtl-toggle ha-icon-button {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-switch";
|
||||
|
||||
@@ -50,59 +51,100 @@ export class DemoHaControlSwitch extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${repeat(switches, (sw) => {
|
||||
const { id, label, ...config } = sw;
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<label id=${id}>${label}</label>
|
||||
<pre>Config: ${JSON.stringify(config)}</pre>
|
||||
<ha-control-switch
|
||||
.checked=${this.checked}
|
||||
class=${ifDefined(config.class)}
|
||||
@change=${this.handleValueChanged}
|
||||
.pathOn=${mdiLightbulb}
|
||||
.pathOff=${mdiLightbulbOff}
|
||||
.label=${label}
|
||||
?disabled=${config.disabled}
|
||||
?reversed=${config.reversed}
|
||||
>
|
||||
</ha-control-switch>
|
||||
<div class="themes">
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-control-switch ${mode}">
|
||||
${repeat(switches, (sw) => {
|
||||
const { id, label, ...config } = sw;
|
||||
return html`
|
||||
<div class="card-content">
|
||||
<label id="${mode}-${id}">${label}</label>
|
||||
<pre>Config: ${JSON.stringify(config)}</pre>
|
||||
<ha-control-switch
|
||||
.checked=${this.checked}
|
||||
class=${ifDefined(config.class)}
|
||||
@change=${this.handleValueChanged}
|
||||
.pathOn=${mdiLightbulb}
|
||||
.pathOff=${mdiLightbulbOff}
|
||||
.label=${label}
|
||||
?disabled=${config.disabled}
|
||||
?reversed=${config.reversed}
|
||||
>
|
||||
</ha-control-switch>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Vertical</b></p>
|
||||
<div class="vertical-switches">
|
||||
${repeat(switches, (sw) => {
|
||||
const { label, ...config } = sw;
|
||||
return html`
|
||||
<ha-control-switch
|
||||
.checked=${this.checked}
|
||||
vertical
|
||||
class=${ifDefined(config.class)}
|
||||
@change=${this.handleValueChanged}
|
||||
.label=${label}
|
||||
.pathOn=${mdiGarageOpen}
|
||||
.pathOff=${mdiGarage}
|
||||
?disabled=${config.disabled}
|
||||
?reversed=${config.reversed}
|
||||
>
|
||||
</ha-control-switch>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
})}
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Vertical</b></p>
|
||||
<div class="vertical-switches">
|
||||
${repeat(switches, (sw) => {
|
||||
const { id, label, ...config } = sw;
|
||||
return html`
|
||||
<ha-control-switch
|
||||
.checked=${this.checked}
|
||||
vertical
|
||||
class=${ifDefined(config.class)}
|
||||
@change=${this.handleValueChanged}
|
||||
.label=${label}
|
||||
.pathOn=${mdiGarageOpen}
|
||||
.pathOff=${mdiGarage}
|
||||
?disabled=${config.disabled}
|
||||
?reversed=${config.reversed}
|
||||
>
|
||||
</ha-control-switch>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.themes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 16px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
pre {
|
||||
margin-top: 0;
|
||||
|
||||
@@ -27,7 +27,6 @@ export class DemoHaInput extends LitElement {
|
||||
constructor() {
|
||||
super();
|
||||
// Provides internationalizationContext for ha-input-copy, ha-input-multi and ha-input-search
|
||||
// eslint-disable-next-line no-new
|
||||
new ContextProvider(this, {
|
||||
context: internationalizationContext,
|
||||
initialValue: {
|
||||
|
||||
@@ -52,6 +52,7 @@ const SENSOR_DEVICE_CLASSES = [
|
||||
"sulphur_dioxide",
|
||||
"temperature",
|
||||
"timestamp",
|
||||
"uptime",
|
||||
"volatile_organic_compounds",
|
||||
"volatile_organic_compounds_parts",
|
||||
"voltage",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { extractSearchParam } from "../../src/common/url/search-params";
|
||||
import "../../src/components/ha-alert";
|
||||
import "../../src/components/ha-button";
|
||||
import "../../src/components/ha-fade-in";
|
||||
import "../../src/components/animation/ha-fade-in";
|
||||
import "../../src/components/ha-spinner";
|
||||
import "../../src/components/ha-svg-icon";
|
||||
import "../../src/components/progress/ha-progress-bar";
|
||||
|
||||
+17
-19
@@ -33,20 +33,21 @@
|
||||
"@codemirror/lang-jinja": "6.0.1",
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/lint": "6.9.5",
|
||||
"@codemirror/search": "6.7.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.41.1",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.3.2",
|
||||
"@formatjs/intl-displaynames": "7.3.2",
|
||||
"@formatjs/intl-durationformat": "0.10.4",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.3",
|
||||
"@formatjs/intl-listformat": "8.3.2",
|
||||
"@formatjs/intl-locale": "5.3.2",
|
||||
"@formatjs/intl-numberformat": "9.3.2",
|
||||
"@formatjs/intl-pluralrules": "6.3.2",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.2",
|
||||
"@formatjs/intl-datetimeformat": "7.4.0",
|
||||
"@formatjs/intl-displaynames": "7.3.3",
|
||||
"@formatjs/intl-durationformat": "0.10.5",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.4",
|
||||
"@formatjs/intl-listformat": "8.3.3",
|
||||
"@formatjs/intl-locale": "5.3.3",
|
||||
"@formatjs/intl-numberformat": "9.3.3",
|
||||
"@formatjs/intl-pluralrules": "6.3.3",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.3",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -99,7 +100,7 @@
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.2.1",
|
||||
"intl-messageformat": "11.2.2",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -135,7 +136,6 @@
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.2",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.1",
|
||||
"@eslint/eslintrc": "3.3.5",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.59.0",
|
||||
"@lokalise/node-api": "15.7.1",
|
||||
@@ -144,7 +144,7 @@
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.9",
|
||||
"@rspack/core": "2.0.0",
|
||||
"@rspack/dev-server": "2.0.0",
|
||||
"@rspack/dev-server": "2.0.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -162,16 +162,14 @@
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.1.4",
|
||||
"@vitest/coverage-v8": "4.1.5",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.2.1",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-import-x": "4.16.2",
|
||||
"eslint-plugin-lit": "2.2.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
@@ -187,7 +185,7 @@
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "29.0.2",
|
||||
"jsdom": "29.1.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.4.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -200,12 +198,12 @@
|
||||
"serve": "14.2.6",
|
||||
"sinon": "21.1.2",
|
||||
"tar": "7.5.13",
|
||||
"terser-webpack-plugin": "5.4.0",
|
||||
"terser-webpack-plugin": "5.5.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.59.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.4",
|
||||
"vitest": "4.1.5",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.0#~/.yarn/patches/workbox-build-npm-7.4.0-c84561662c.patch"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<svg width="268" height="28" viewBox="0 0 268 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="88" height="28" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="87" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="28" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="48" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="68" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M107.667 18.1167V14.7833H100.233L100.208 13.1083H107.667V9.78333L111.833 13.95L107.667 18.1167Z" fill="#B1B1B1"/>
|
||||
<rect x="124" width="88" height="28" rx="8" fill="white"/>
|
||||
<rect x="124.5" y="0.5" width="87" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="132" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="152" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="172" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="192" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<path d="M237.5 13.1667H222.5V11.5H237.5V13.1667ZM237.5 14.8333H222.5V16.5H237.5V14.8333Z" fill="#B1B1B1"/>
|
||||
<path d="M257.167 16.5H253L258.833 4.83337V11.5H263L257.167 23.1667V16.5Z" fill="#5E5E5E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg width="268" height="28" viewBox="0 0 268 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H80C84.4183 0 88 3.58172 88 8V20C88 24.4183 84.4183 28 80 28H8C3.58172 28 0 24.4183 0 20V8Z" fill="#1C1C1C"/>
|
||||
<path d="M8 0.5H80C84.1421 0.5 87.5 3.85786 87.5 8V20C87.5 24.1421 84.1421 27.5 80 27.5H8C3.85786 27.5 0.5 24.1421 0.5 20V8C0.5 3.85786 3.85786 0.5 8 0.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="28" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="48" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="68" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M109.333 16.45C108.718 17.065 107.667 16.6294 107.667 15.7596C107.667 15.2204 107.23 14.7833 106.69 14.7833H101.058C100.601 14.7833 100.228 14.4159 100.221 13.9583C100.214 13.4909 100.591 13.1083 101.058 13.1083H106.693C107.231 13.1083 107.667 12.6723 107.667 12.1345C107.667 11.2668 108.716 10.8323 109.329 11.4458L110.613 12.7296C111.287 13.4036 111.287 14.4964 110.613 15.1704L109.333 16.45Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M124 8C124 3.58172 127.582 0 132 0H204C208.418 0 212 3.58172 212 8V20C212 24.4183 208.418 28 204 28H132C127.582 28 124 24.4183 124 20V8Z" fill="#1C1C1C"/>
|
||||
<path d="M132 0.5H204C208.142 0.5 211.5 3.85786 211.5 8V20C211.5 24.1421 208.142 27.5 204 27.5H132C127.858 27.5 124.5 24.1421 124.5 20V8C124.5 3.85786 127.858 0.5 132 0.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="132" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="152" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="172" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="192" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<path d="M237.5 12.3333C237.5 12.7936 237.127 13.1667 236.667 13.1667H223.333C222.873 13.1667 222.5 12.7936 222.5 12.3333C222.5 11.8731 222.873 11.5 223.333 11.5H236.667C237.127 11.5 237.5 11.8731 237.5 12.3333ZM237.5 15.6667C237.5 15.2064 237.127 14.8333 236.667 14.8333H223.333C222.873 14.8333 222.5 15.2064 222.5 15.6667C222.5 16.1269 222.873 16.5 223.333 16.5H236.667C237.127 16.5 237.5 16.1269 237.5 15.6667Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M257.167 16.5H253L258.833 4.83337V11.5H263L257.167 23.1667V16.5Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg width="268" height="28" viewBox="0 0 268 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="88" height="28" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="87" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="28" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="48" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="68" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M107.667 18.1167V14.7833H100.233L100.208 13.1083H107.667V9.78333L111.833 13.95L107.667 18.1167Z" fill="#B1B1B1"/>
|
||||
<rect x="124" width="88" height="28" rx="8" fill="white"/>
|
||||
<rect x="124.5" y="0.5" width="87" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="132" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="152" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="172" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="192" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M237.5 13.1667H222.5V11.5H237.5V13.1667ZM237.5 14.8333H222.5V16.5H237.5V14.8333Z" fill="#B1B1B1"/>
|
||||
<path d="M257.167 16.5H253L258.833 4.83337V11.5H263L257.167 23.1667V16.5Z" fill="#5E5E5E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg width="268" height="28" viewBox="0 0 268 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H80C84.4183 0 88 3.58172 88 8V20C88 24.4183 84.4183 28 80 28H8C3.58172 28 0 24.4183 0 20V8Z" fill="#1C1C1C"/>
|
||||
<path d="M8 0.5H80C84.1421 0.5 87.5 3.85786 87.5 8V20C87.5 24.1421 84.1421 27.5 80 27.5H8C3.85786 27.5 0.5 24.1421 0.5 20V8C0.5 3.85786 3.85786 0.5 8 0.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="28" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="48" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="68" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M109.333 16.45C108.718 17.065 107.667 16.6294 107.667 15.7596C107.667 15.2204 107.23 14.7833 106.69 14.7833H101.058C100.601 14.7833 100.228 14.4159 100.221 13.9583C100.214 13.4909 100.591 13.1083 101.058 13.1083H106.693C107.231 13.1083 107.667 12.6723 107.667 12.1345C107.667 11.2668 108.716 10.8323 109.329 11.4458L110.613 12.7296C111.287 13.4036 111.287 14.4964 110.613 15.1704L109.333 16.45Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M124 8C124 3.58172 127.582 0 132 0H204C208.418 0 212 3.58172 212 8V20C212 24.4183 208.418 28 204 28H132C127.582 28 124 24.4183 124 20V8Z" fill="#1C1C1C"/>
|
||||
<path d="M132 0.5H204C208.142 0.5 211.5 3.85786 211.5 8V20C211.5 24.1421 208.142 27.5 204 27.5H132C127.858 27.5 124.5 24.1421 124.5 20V8C124.5 3.85786 127.858 0.5 132 0.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="132" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="152" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="172" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="192" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M237.5 12.3333C237.5 12.7936 237.127 13.1667 236.667 13.1667H223.333C222.873 13.1667 222.5 12.7936 222.5 12.3333C222.5 11.8731 222.873 11.5 223.333 11.5H236.667C237.127 11.5 237.5 11.8731 237.5 12.3333ZM237.5 15.6667C237.5 15.2064 237.127 14.8333 236.667 14.8333H223.333C222.873 14.8333 222.5 15.2064 222.5 15.6667C222.5 16.1269 222.873 16.5 223.333 16.5H236.667C237.127 16.5 237.5 16.1269 237.5 15.6667Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M257.167 16.5H253L258.833 4.83337V11.5H263L257.167 23.1667V16.5Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg width="268" height="28" viewBox="0 0 268 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="88" height="28" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="87" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="28" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="48" y="8" width="12" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="68" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<path d="M107.667 18.1167V14.7833H100.233L100.208 13.1083H107.667V9.78333L111.833 13.95L107.667 18.1167Z" fill="#B1B1B1"/>
|
||||
<rect x="124" width="88" height="28" rx="8" fill="white"/>
|
||||
<rect x="124.5" y="0.5" width="87" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="132" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="152" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="172" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="192" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<path d="M237.5 13.1667H222.5V11.5H237.5V13.1667ZM237.5 14.8333H222.5V16.5H237.5V14.8333Z" fill="#B1B1B1"/>
|
||||
<path d="M257.167 16.5H253L258.833 4.83337V11.5H263L257.167 23.1667V16.5Z" fill="#5E5E5E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg width="268" height="28" viewBox="0 0 268 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H80C84.4183 0 88 3.58172 88 8V20C88 24.4183 84.4183 28 80 28H8C3.58172 28 0 24.4183 0 20V8Z" fill="#1C1C1C"/>
|
||||
<path d="M8 0.5H80C84.1421 0.5 87.5 3.85786 87.5 8V20C87.5 24.1421 84.1421 27.5 80 27.5H8C3.85786 27.5 0.5 24.1421 0.5 20V8C0.5 3.85786 3.85786 0.5 8 0.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="28" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="48" y="8" width="12" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="68" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<path d="M109.333 16.45C108.718 17.065 107.667 16.6294 107.667 15.7596C107.667 15.2204 107.23 14.7833 106.69 14.7833H101.058C100.601 14.7833 100.228 14.4159 100.221 13.9583C100.214 13.4909 100.591 13.1083 101.058 13.1083H106.693C107.231 13.1083 107.667 12.6723 107.667 12.1345C107.667 11.2668 108.716 10.8323 109.329 11.4458L110.613 12.7296C111.287 13.4036 111.287 14.4964 110.613 15.1704L109.333 16.45Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M124 8C124 3.58172 127.582 0 132 0H204C208.418 0 212 3.58172 212 8V20C212 24.4183 208.418 28 204 28H132C127.582 28 124 24.4183 124 20V8Z" fill="#1C1C1C"/>
|
||||
<path d="M132 0.5H204C208.142 0.5 211.5 3.85786 211.5 8V20C211.5 24.1421 208.142 27.5 204 27.5H132C127.858 27.5 124.5 24.1421 124.5 20V8C124.5 3.85786 127.858 0.5 132 0.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="132" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="152" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="172" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<rect x="192" y="8" width="12" height="12" rx="3" fill="#009AC7"/>
|
||||
<path d="M237.5 12.3333C237.5 12.7936 237.127 13.1667 236.667 13.1667H223.333C222.873 13.1667 222.5 12.7936 222.5 12.3333C222.5 11.8731 222.873 11.5 223.333 11.5H236.667C237.127 11.5 237.5 11.8731 237.5 12.3333ZM237.5 15.6667C237.5 15.2064 237.127 14.8333 236.667 14.8333H223.333C222.873 14.8333 222.5 15.2064 222.5 15.6667C222.5 16.1269 222.873 16.5 223.333 16.5H236.667C237.127 16.5 237.5 16.1269 237.5 15.6667Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M257.167 16.5H253L258.833 4.83337V11.5H263L257.167 23.1667V16.5Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260325.0"
|
||||
version = "20260429.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -44,9 +44,9 @@ export const normalizeLuminance = (color: string): string => {
|
||||
return midL;
|
||||
}
|
||||
if (testLuminance < targetLuminance) {
|
||||
return findLightness(midL, highL, iterations--);
|
||||
return findLightness(midL, highL, iterations - 1);
|
||||
}
|
||||
return findLightness(lowL, midL, iterations--);
|
||||
return findLightness(lowL, midL, iterations - 1);
|
||||
}
|
||||
|
||||
baseOklch.l = findLightness();
|
||||
|
||||
@@ -21,8 +21,9 @@ export const closestWithProperty = (
|
||||
own
|
||||
? Object.prototype.hasOwnProperty.call(element, property)
|
||||
: element && property in element
|
||||
)
|
||||
) {
|
||||
return element;
|
||||
}
|
||||
return closestWithProperty(element, property, own);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
import { computeRTL } from "../util/compute_rtl";
|
||||
import { computeAreaName } from "./compute_area_name";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeEntityName, entityUseDeviceName } from "./compute_entity_name";
|
||||
@@ -117,3 +118,32 @@ export const computeEntityNameList = (
|
||||
|
||||
return names;
|
||||
};
|
||||
|
||||
export interface EntityPickerDisplay {
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
}
|
||||
|
||||
export const computeEntityPickerDisplay = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: HassEntity
|
||||
): EntityPickerDisplay => {
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(hass);
|
||||
|
||||
const primary = entityName || deviceName || stateObj.entity_id;
|
||||
const secondary =
|
||||
[areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ") || undefined;
|
||||
|
||||
return { primary, secondary };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { isUnavailableState } from "../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface EntityUnitStubConfig {
|
||||
entity: string;
|
||||
attribute?: string;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the display unit for an entity.
|
||||
*
|
||||
* @param hass - Home Assistant instance
|
||||
* @param stateObj - Entity state object
|
||||
* @param config - Element configuration
|
||||
* @returns Computed entity unit
|
||||
*/
|
||||
export const computeEntityUnitDisplay = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: HassEntity | undefined,
|
||||
config: EntityUnitStubConfig
|
||||
): string => {
|
||||
let unit;
|
||||
if (
|
||||
stateObj &&
|
||||
!isUnavailableState(stateObj.state) &&
|
||||
(config.attribute || stateObj.attributes.device_class !== "duration")
|
||||
) {
|
||||
// check for an explicitly defined unit in config
|
||||
unit = config.unit;
|
||||
|
||||
if (!unit) {
|
||||
if (!config.attribute) {
|
||||
// use entity's unit_of_measurement
|
||||
const stateParts = hass.formatEntityStateToParts(stateObj);
|
||||
unit = stateParts.find((part) => part.type === "unit")?.value;
|
||||
} else {
|
||||
// use attribute's unit if available
|
||||
const attrParts = hass.formatEntityAttributeValueToParts(
|
||||
stateObj,
|
||||
config.attribute
|
||||
);
|
||||
unit = attrParts.find((part) => part.type === "unit")?.value;
|
||||
}
|
||||
}
|
||||
|
||||
return unit ?? "";
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
@@ -258,6 +258,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
"infrared",
|
||||
"input_button",
|
||||
"notify",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
@@ -265,7 +266,9 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
"wake_word",
|
||||
"datetime",
|
||||
].includes(domain) ||
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
(domain === "sensor" &&
|
||||
(attributes.device_class === "timestamp" ||
|
||||
attributes.device_class === "uptime"))
|
||||
) {
|
||||
try {
|
||||
return [
|
||||
|
||||
@@ -54,6 +54,7 @@ export const FIXED_DOMAIN_STATES = {
|
||||
],
|
||||
person: ["home", "not_home"],
|
||||
plant: ["ok", "problem"],
|
||||
radio_frequency: [],
|
||||
remote: ["on", "off"],
|
||||
scene: [],
|
||||
schedule: ["on", "off"],
|
||||
@@ -224,6 +225,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
"sulphur_dioxide",
|
||||
"temperature",
|
||||
"timestamp",
|
||||
"uptime",
|
||||
"volatile_organic_compounds",
|
||||
"volatile_organic_compounds_parts",
|
||||
"voltage",
|
||||
|
||||
@@ -7,7 +7,14 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
|
||||
if (
|
||||
["button", "event", "infrared", "input_button", "scene"].includes(domain)
|
||||
[
|
||||
"button",
|
||||
"event",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
].includes(domain)
|
||||
) {
|
||||
return compareState !== UNAVAILABLE;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ const SECS_PER_HOUR = SECS_PER_MIN * 60;
|
||||
// Adapted from https://github.com/formatjs/formatjs/blob/186cef62f980ec66252ee232f438a42d0b51b9f9/packages/intl-utils/src/diff.ts
|
||||
export function selectUnit(
|
||||
from: Date | number,
|
||||
// eslint-disable-next-line default-param-last
|
||||
to: Date | number = Date.now(),
|
||||
locale: FrontendLocaleData,
|
||||
thresholds: Partial<Thresholds> = {}
|
||||
|
||||
@@ -3,13 +3,14 @@ import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-fade-in")
|
||||
export class HaFadeIn extends WaAnimation {
|
||||
@property() public name = "fadeIn";
|
||||
|
||||
@property() public fill: FillMode = "both";
|
||||
|
||||
@property({ type: Boolean }) public play = true;
|
||||
|
||||
@property({ type: Number }) public iterations = 1;
|
||||
constructor() {
|
||||
super();
|
||||
this.iterations = 1;
|
||||
this.fill = "both";
|
||||
this.name = "fadeIn";
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -0,0 +1,20 @@
|
||||
import WaAnimation from "@home-assistant/webawesome/dist/components/animation/animation";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-fade-out")
|
||||
export class HaFadeOut extends WaAnimation {
|
||||
@property({ type: Boolean }) public play = true;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.iterations = 1;
|
||||
this.fill = "both";
|
||||
this.name = "fadeOut";
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-fade-out": HaFadeOut;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import "@home-assistant/webawesome/dist/components/animation/animation";
|
||||
import { mdiInformationOutline } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import "../animation/ha-fade-in";
|
||||
import "../animation/ha-fade-out";
|
||||
import "../ha-icon-button";
|
||||
|
||||
@customElement("ha-automation-row-event-chip")
|
||||
export class HaAutomationRowEventChip extends LitElement {
|
||||
@property({ reflect: true })
|
||||
public variant: "info" | "warning" | "success" | "danger" = "info";
|
||||
|
||||
@property({ type: Boolean })
|
||||
public interactive = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public show = false;
|
||||
|
||||
@state()
|
||||
private _hide = false;
|
||||
|
||||
@state()
|
||||
private _highlight = 0;
|
||||
|
||||
willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("show")) {
|
||||
this._highlight = 0;
|
||||
|
||||
if (!this.show && this.hasUpdated) {
|
||||
this._hide = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this.show && !this._hide) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let base = html`<div><slot></slot></div>`;
|
||||
|
||||
if (this.interactive) {
|
||||
base = html`<button>
|
||||
<slot></slot>
|
||||
<ha-svg-icon .path=${mdiInformationOutline}></ha-svg-icon>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
if (this.show && this._highlight) {
|
||||
return keyed(
|
||||
this._highlight,
|
||||
html`
|
||||
<wa-animation fill="both" .iterations=${1} name="tada" play
|
||||
>${base}</wa-animation
|
||||
>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.show && this._hide) {
|
||||
return html`
|
||||
<ha-fade-out @wa-finish=${this._handleHideFinish}>${base}</ha-fade-out>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`<ha-fade-in .duration=${250}>${base}</ha-fade-in>`;
|
||||
}
|
||||
|
||||
public highlight() {
|
||||
this._highlight += 1;
|
||||
}
|
||||
|
||||
private _handleHideFinish() {
|
||||
this._hide = false;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--background-color: var(--ha-color-fill-primary-normal-resting);
|
||||
--background-color-hover: var(--ha-color-fill-primary-normal-hover);
|
||||
--text-color: var(--ha-color-on-primary-normal);
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
}
|
||||
|
||||
:host([variant="warning"]) {
|
||||
--background-color: var(--ha-color-fill-warning-normal-resting);
|
||||
--background-color-hover: var(--ha-color-fill-warning-normal-hover);
|
||||
--text-color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
|
||||
:host([variant="success"]) {
|
||||
--background-color: var(--ha-color-fill-success-normal-resting);
|
||||
--background-color-hover: var(--ha-color-fill-success-normal-hover);
|
||||
--text-color: var(--ha-color-on-success-normal);
|
||||
}
|
||||
|
||||
:host([variant="danger"]) {
|
||||
--background-color: var(--ha-color-fill-danger-normal-resting);
|
||||
--background-color-hover: var(--ha-color-fill-danger-normal-hover);
|
||||
--text-color: var(--ha-color-on-danger-normal);
|
||||
}
|
||||
|
||||
button,
|
||||
div {
|
||||
background: var(--background-color);
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
color: var(--text-color);
|
||||
display: inline-flex;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
align-items: center;
|
||||
--mdc-icon-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--background-color-hover);
|
||||
}
|
||||
|
||||
button:focus-visible {
|
||||
outline: var(--wa-focus-ring);
|
||||
outline-offset: var(--wa-focus-ring-offset);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-row-event-chip": HaAutomationRowEventChip;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"toggle-collapsed": undefined;
|
||||
"stop-sort-selection": undefined;
|
||||
"copy-row": undefined;
|
||||
"cut-row": undefined;
|
||||
"delete-row": undefined;
|
||||
}
|
||||
}
|
||||
+36
-4
@@ -2,8 +2,8 @@ import { mdiChevronUp } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-icon-button";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-icon-button";
|
||||
|
||||
@customElement("ha-automation-row")
|
||||
export class HaAutomationRow extends LitElement {
|
||||
@@ -27,6 +27,9 @@ export class HaAutomationRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public highlight?: boolean;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public dim = false;
|
||||
|
||||
@query(".row")
|
||||
private _rowElement?: HTMLDivElement;
|
||||
|
||||
@@ -51,7 +54,11 @@ export class HaAutomationRow extends LitElement {
|
||||
<div class="leading-icon-wrapper">
|
||||
<slot name="leading-icon"></slot>
|
||||
</div>
|
||||
<slot class="header" name="header"></slot>
|
||||
<div class="header">
|
||||
<slot name="header"></slot>
|
||||
<slot name="event"></slot>
|
||||
</div>
|
||||
|
||||
<div class="icons">
|
||||
<slot name="icons"></slot>
|
||||
</div>
|
||||
@@ -172,12 +179,24 @@ export class HaAutomationRow extends LitElement {
|
||||
border-top-right-radius: var(--ha-border-radius-square);
|
||||
border-top-left-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
::slotted([slot="header"]) {
|
||||
.header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
margin: 0 var(--ha-space-3);
|
||||
}
|
||||
::slotted([slot="header"]) {
|
||||
overflow-wrap: anywhere;
|
||||
margin: 0 var(--ha-space-3);
|
||||
}
|
||||
::slotted([slot="event"]) {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
.icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -199,6 +218,19 @@ export class HaAutomationRow extends LitElement {
|
||||
:host([highlight]) .row:hover {
|
||||
background-color: rgba(var(--rgb-primary-color), 0.16);
|
||||
}
|
||||
|
||||
.icons,
|
||||
.leading-icon-wrapper,
|
||||
::slotted([slot="header"]) {
|
||||
transition: opacity var(--ha-animation-duration-normal);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:host([dim]) .icons,
|
||||
:host([dim]) .leading-icon-wrapper,
|
||||
:host([dim]) ::slotted([slot="header"]) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiChevronDown, mdiChevronUp, mdiRestart } from "@mdi/js";
|
||||
import {
|
||||
mdiCheckCircle,
|
||||
mdiChevronDown,
|
||||
mdiChevronUp,
|
||||
mdiCircleOutline,
|
||||
mdiRestart,
|
||||
} from "@mdi/js";
|
||||
import { differenceInMinutes } from "date-fns";
|
||||
import type { DataZoomComponentOption } from "echarts/components";
|
||||
import type { EChartsType } from "echarts/core";
|
||||
@@ -47,6 +53,10 @@ export type CustomLegendOption = ECOption["legend"] & {
|
||||
name: string;
|
||||
value?: string; // Current value to display next to the name in the legend.
|
||||
itemStyle?: Record<string, any>;
|
||||
// If true, label click does not fire `legend-label-click` even when the
|
||||
// chart has `clickLabelForMoreInfo`; falls back to toggle. Used for items
|
||||
// without a corresponding entity (e.g. external statistics).
|
||||
noLabelClick?: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -81,6 +91,9 @@ export class HaChartBase extends LitElement {
|
||||
})
|
||||
private _themes!: Themes;
|
||||
|
||||
@property({ attribute: "click-label-for-more-info", type: Boolean })
|
||||
public clickLabelForMoreInfo = false;
|
||||
|
||||
@state() private _isZoomed = false;
|
||||
|
||||
@state() private _zoomRatio = 1;
|
||||
@@ -360,18 +373,19 @@ export class HaChartBase extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
let itemStyle: Record<string, any> = {};
|
||||
let name = "";
|
||||
let id = "";
|
||||
let value = "";
|
||||
let noLabelClick = false;
|
||||
const name = typeof item === "string" ? item : (item.name ?? "");
|
||||
if (typeof item === "string") {
|
||||
name = item;
|
||||
id = item;
|
||||
} else {
|
||||
name = item.name ?? "";
|
||||
id = item.id ?? name;
|
||||
value = item.value ?? "";
|
||||
itemStyle = item.itemStyle ?? {};
|
||||
noLabelClick = item.noLabelClick ?? false;
|
||||
}
|
||||
const labelClickable = this.clickLabelForMoreInfo && !noLabelClick;
|
||||
const dataset =
|
||||
datasets.find((d) => d.id === id) ??
|
||||
datasets.find((d) => d.name === id);
|
||||
@@ -381,26 +395,43 @@ export class HaChartBase extends LitElement {
|
||||
...itemStyle,
|
||||
};
|
||||
const color = itemStyle?.color as string;
|
||||
const borderColor = itemStyle?.borderColor as string;
|
||||
return html`<li
|
||||
.id=${id}
|
||||
@click=${this._legendClick}
|
||||
@pointerdown=${this._legendPointerDown}
|
||||
@pointerup=${this._legendPointerCancel}
|
||||
@pointerleave=${this._legendPointerCancel}
|
||||
@pointercancel=${this._legendPointerCancel}
|
||||
@contextmenu=${this._legendContextMenu}
|
||||
class=${classMap({ hidden: this._hiddenDatasets.has(id) })}
|
||||
.title=${name}
|
||||
>
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
backgroundColor: color,
|
||||
borderColor: borderColor || color,
|
||||
})}
|
||||
></div>
|
||||
<div class="label">${name}</div>
|
||||
<button
|
||||
type="button"
|
||||
class="legend-toggle"
|
||||
data-id=${id}
|
||||
aria-pressed=${!this._hiddenDatasets.has(id)}
|
||||
.title=${this.hass.localize(
|
||||
"ui.components.history_charts.toggle_visibility"
|
||||
)}
|
||||
@click=${this._toggleDataset}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this._hiddenDatasets.has(id)
|
||||
? mdiCircleOutline
|
||||
: mdiCheckCircle}
|
||||
style=${styleMap({
|
||||
color: this._hiddenDatasets.has(id) ? undefined : color,
|
||||
})}
|
||||
></ha-svg-icon>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class=${classMap({ label: true, clickable: labelClickable })}
|
||||
data-id=${id}
|
||||
.title=${name}
|
||||
@click=${this._labelClick}
|
||||
>
|
||||
${name}
|
||||
</button>
|
||||
${value ? html`<div class="value">${value}</div>` : nothing}
|
||||
</li>`;
|
||||
})}
|
||||
@@ -1163,7 +1194,8 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _legendClick(ev: MouseEvent) {
|
||||
private _toggleDataset(ev: MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
@@ -1171,13 +1203,46 @@ export class HaChartBase extends LitElement {
|
||||
this._longPressTriggered = false;
|
||||
return;
|
||||
}
|
||||
const id = (ev.currentTarget as HTMLElement)?.id;
|
||||
const id = (ev.currentTarget as HTMLElement).dataset.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
// Cmd+click on Mac (Ctrl+click is right-click there), Ctrl+click elsewhere
|
||||
const soloModifier = isMac ? ev.metaKey : ev.ctrlKey;
|
||||
if (soloModifier) {
|
||||
this._soloLegend(id);
|
||||
return;
|
||||
}
|
||||
this._handleDatasetToggle(id);
|
||||
}
|
||||
|
||||
private _labelClick(ev: MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (this._longPressTriggered) {
|
||||
this._longPressTriggered = false;
|
||||
return;
|
||||
}
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
const id = target.dataset.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const soloModifier = isMac ? ev.metaKey : ev.ctrlKey;
|
||||
if (soloModifier) {
|
||||
this._soloLegend(id);
|
||||
return;
|
||||
}
|
||||
if (target.classList.contains("clickable")) {
|
||||
fireEvent(this, "legend-label-click", { id });
|
||||
} else {
|
||||
this._handleDatasetToggle(id);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleDatasetToggle(id: string) {
|
||||
if (this._hiddenDatasets.has(id)) {
|
||||
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
|
||||
this._hiddenDatasets.delete(i)
|
||||
@@ -1392,7 +1457,6 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
.chart-legend li {
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 2px;
|
||||
@@ -1409,9 +1473,26 @@ export class HaChartBase extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.chart-legend .label {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.chart-legend .label.clickable:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.chart-legend .legend-toggle:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
.chart-legend .value {
|
||||
color: var(--secondary-text-color);
|
||||
@@ -1419,23 +1500,26 @@ export class HaChartBase extends LitElement {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chart-legend .bullet {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
display: block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: 4px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
.chart-legend .legend-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
margin: -4px;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
.chart-legend .hidden .bullet {
|
||||
border-color: var(--secondary-text-color) !important;
|
||||
background-color: transparent !important;
|
||||
.chart-legend .legend-toggle:focus-visible,
|
||||
.chart-legend .label:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--ha-border-radius-small, 4px);
|
||||
}
|
||||
.chart-legend .legend-toggle ha-svg-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
ha-assist-chip {
|
||||
height: 100%;
|
||||
@@ -1455,6 +1539,7 @@ declare global {
|
||||
"dataset-hidden": { id: string };
|
||||
"dataset-unhidden": { id: string };
|
||||
"chart-click": ECElementEvent;
|
||||
"legend-label-click": { id: string };
|
||||
"chart-zoom": {
|
||||
start: number;
|
||||
end: number;
|
||||
|
||||
@@ -291,20 +291,26 @@ export class HaSankeyChart extends LitElement {
|
||||
}
|
||||
|
||||
private _findParentIndex(id: string, links: Link[], sections: Node[][]) {
|
||||
const parent = links.find((l) => l.target === id)?.source;
|
||||
if (!parent) {
|
||||
const parents = links.filter((l) => l.target === id).map((l) => l.source);
|
||||
if (parents.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
let offset = 0;
|
||||
for (let i = sections.length - 1; i >= 0; i--) {
|
||||
const section = sections[i];
|
||||
const index = section.findIndex((n) => n.id === parent);
|
||||
if (index !== -1) {
|
||||
return offset + index;
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
for (const parent of parents) {
|
||||
let offset = 0;
|
||||
for (let i = sections.length - 1; i >= 0; i--) {
|
||||
const section = sections[i];
|
||||
const index = section.findIndex((n) => n.id === parent);
|
||||
if (index !== -1) {
|
||||
sum += offset + index;
|
||||
count++;
|
||||
break;
|
||||
}
|
||||
offset += section.length;
|
||||
}
|
||||
offset += section.length;
|
||||
}
|
||||
return -1;
|
||||
return count > 0 ? sum / count : -1;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -18,10 +18,12 @@ import {
|
||||
formatNumber,
|
||||
} from "../../common/number/format_number";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { computeAttributeValueDisplay } from "../../common/entity/compute_attribute_display";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
@@ -35,6 +37,21 @@ const CLIMATE_MODE_CONFIGS = [
|
||||
{ mode: "fan_only", action: "fan", cssVar: "--state-climate-fan_only-color" },
|
||||
] as const;
|
||||
|
||||
// Used to recover the underlying entity_id from a legend dataset id.
|
||||
// Kept in sync with the suffixes appended at dataset construction below
|
||||
// for climate / water_heater / humidifier multi-attribute charts.
|
||||
const ENTITY_DATASET_SUFFIXES = [
|
||||
"-current_temperature",
|
||||
"-target_temperature",
|
||||
"-target_temperature_mode",
|
||||
"-target_temperature_mode_low",
|
||||
...CLIMATE_MODE_CONFIGS.map((c) => `-${c.action}`),
|
||||
"-current_humidity",
|
||||
"-target_humidity",
|
||||
"-humidifying",
|
||||
"-on",
|
||||
];
|
||||
|
||||
@customElement("state-history-chart-line")
|
||||
export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -111,6 +128,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
.expandLegend=${this.expandLegend}
|
||||
.hideResetButton=${this.hideResetButton}
|
||||
.clickLabelForMoreInfo=${this.clickForMoreInfo}
|
||||
@legend-label-click=${this._handleLegendLabelClick}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
@@ -128,8 +147,9 @@ export class StateHistoryChartLine extends LitElement {
|
||||
if (
|
||||
dataset.tooltip?.show === false ||
|
||||
this._hiddenStats.has(dataset.id as string)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const param = params.find(
|
||||
(p: Record<string, any>) => p.seriesIndex === index
|
||||
);
|
||||
@@ -221,6 +241,24 @@ export class StateHistoryChartLine extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleLegendLabelClick(
|
||||
ev: HASSDomEvent<HASSDomEvents["legend-label-click"]>
|
||||
) {
|
||||
const id = ev.detail.id;
|
||||
let entityId = id;
|
||||
if (!this.hass.states[entityId]) {
|
||||
for (const suffix of ENTITY_DATASET_SUFFIXES) {
|
||||
if (id.endsWith(suffix)) {
|
||||
entityId = id.slice(0, -suffix.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.hass.states[entityId]) {
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
changedProps.has("data") ||
|
||||
@@ -310,12 +348,50 @@ export class StateHistoryChartLine extends LitElement {
|
||||
.filter((item) => !(item.dataset as LineSeriesOption).areaStyle)
|
||||
.map((item) => {
|
||||
const stateObj = this.hass.states[item.entityId];
|
||||
let value: string | undefined;
|
||||
|
||||
if (stateObj) {
|
||||
// For climate temperature datasets, show temperature values
|
||||
const datasetId = item.dataset.id as string;
|
||||
if (
|
||||
datasetId?.endsWith("-current_temperature") ||
|
||||
datasetId?.endsWith("-target_temperature") ||
|
||||
datasetId?.endsWith("-target_temperature_mode") ||
|
||||
datasetId?.endsWith("-target_temperature_mode_low")
|
||||
) {
|
||||
let attribute: string | undefined;
|
||||
if (datasetId.endsWith("-current_temperature")) {
|
||||
attribute = "current_temperature";
|
||||
} else if (
|
||||
datasetId.endsWith("-target_temperature_mode_low")
|
||||
) {
|
||||
attribute = "target_temp_low";
|
||||
} else if (datasetId.endsWith("-target_temperature_mode")) {
|
||||
attribute = "target_temp_high";
|
||||
} else {
|
||||
attribute = "temperature";
|
||||
}
|
||||
// Use the helper to format temperature with proper unit
|
||||
value = computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
attribute
|
||||
);
|
||||
}
|
||||
|
||||
// Default for non-temperature datasets / missing attribute
|
||||
if (value === undefined) {
|
||||
value = this.hass.formatEntityState(stateObj);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.dataset.id as string,
|
||||
name: item.dataset.name as string,
|
||||
value: stateObj
|
||||
? this.hass.formatEntityState(stateObj)
|
||||
: undefined,
|
||||
value: value,
|
||||
};
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -10,6 +10,8 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
@@ -46,6 +48,13 @@ export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
change: "sum",
|
||||
};
|
||||
|
||||
// When the chart has a single entity, ha-chart-base falls back to raw series
|
||||
// ids (`${statistic_id}-${type}`) for the legend (see _legendData branch at
|
||||
// the bottom of _generateData). Strip the type suffix to recover statistic_id.
|
||||
const STAT_TYPE_SUFFIXES = (
|
||||
Object.keys(supportedStatTypeMap) as StatisticType[]
|
||||
).map((t) => `-${t}`);
|
||||
|
||||
@customElement("statistics-chart")
|
||||
export class StatisticsChart extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -186,6 +195,9 @@ export class StatisticsChart extends LitElement {
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
.expandLegend=${this.expandLegend}
|
||||
.clickLabelForMoreInfo=${this.clickForMoreInfo &&
|
||||
!this._statisticIds.every(isExternalStatistic)}
|
||||
@legend-label-click=${this._handleLegendLabelClick}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
@@ -200,6 +212,28 @@ export class StatisticsChart extends LitElement {
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
|
||||
private _handleLegendLabelClick(
|
||||
ev: HASSDomEvent<HASSDomEvents["legend-label-click"]>
|
||||
) {
|
||||
const id = ev.detail.id;
|
||||
// External statistics aren't real entities; nothing to open.
|
||||
if (isExternalStatistic(id)) {
|
||||
return;
|
||||
}
|
||||
let entityId = id;
|
||||
if (!this.hass.states[entityId]) {
|
||||
for (const suffix of STAT_TYPE_SUFFIXES) {
|
||||
if (id.endsWith(suffix)) {
|
||||
entityId = id.slice(0, -suffix.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.hass.states[entityId]) {
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
}
|
||||
|
||||
private _renderTooltip = (params: any) => {
|
||||
const rendered: Record<string, boolean> = {};
|
||||
const unit = this.unit
|
||||
@@ -400,6 +434,7 @@ export class StatisticsChart extends LitElement {
|
||||
name: string;
|
||||
color?: ZRColor;
|
||||
borderColor?: ZRColor;
|
||||
noLabelClick?: boolean;
|
||||
}[] = [];
|
||||
const statisticIds: string[] = [];
|
||||
let endTime: Date;
|
||||
@@ -603,6 +638,7 @@ export class StatisticsChart extends LitElement {
|
||||
name,
|
||||
color: series.color as ZRColor,
|
||||
borderColor: series.itemStyle?.borderColor,
|
||||
noLabelClick: isExternalStatistic(statistic_id),
|
||||
});
|
||||
}
|
||||
displayedLegend = displayedLegend || showLegend;
|
||||
@@ -738,7 +774,11 @@ export class StatisticsChart extends LitElement {
|
||||
// only update the legend if it has changed or it will trigger options update
|
||||
this._legendData =
|
||||
legendData.length > 1
|
||||
? legendData.map(({ id, name }) => ({ id, name }))
|
||||
? legendData.map(({ id, name, noLabelClick }) => ({
|
||||
id,
|
||||
name,
|
||||
noLabelClick,
|
||||
}))
|
||||
: // if there is only one entity, let the base chart handle the legend
|
||||
undefined;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiPlus, mdiShape } from "@mdi/js";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import {
|
||||
entityComboBoxKeys,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
type EntityComboBoxItem,
|
||||
} from "../../data/entity/entity_picker";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { EntitySelectorExtraOption } from "../../data/selector";
|
||||
import {
|
||||
isHelperDomain,
|
||||
type HelperDomain,
|
||||
@@ -22,6 +22,7 @@ import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-icon";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
@@ -111,23 +112,91 @@ export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
/**
|
||||
* Extra options shown alongside entities. The `id` is used as the value
|
||||
* when the option is selected (it does not need to be a valid entity id).
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public extraOptions?: EntitySelectorExtraOption[];
|
||||
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@property({ attribute: "add-button", type: Boolean })
|
||||
public addButton = false;
|
||||
|
||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@state() private _pendingEntityId?: string;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (
|
||||
this._pendingEntityId &&
|
||||
changedProperties.has("hass") &&
|
||||
this.hass.states !== changedProperties.get("hass")?.states &&
|
||||
this.hass.states[this._pendingEntityId]
|
||||
) {
|
||||
this._setValue(this._pendingEntityId);
|
||||
this._pendingEntityId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
// Load title translations so it is available when the combo-box opens
|
||||
this.hass.loadBackendTranslation("title");
|
||||
}
|
||||
|
||||
private _findExtraOption(value: string | undefined) {
|
||||
return value
|
||||
? this.extraOptions?.find((opt) => opt.id === value)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _renderExtraOptionStart(extraOption: EntitySelectorExtraOption) {
|
||||
const stateObj = extraOption.entity_id
|
||||
? this.hass.states[extraOption.entity_id]
|
||||
: undefined;
|
||||
if (stateObj) {
|
||||
return html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`;
|
||||
}
|
||||
if (extraOption.icon_path) {
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${extraOption.icon_path}
|
||||
style="margin: 0 4px"
|
||||
></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
if (extraOption.icon) {
|
||||
return html`<ha-icon slot="start" .icon=${extraOption.icon}></ha-icon>`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value) => {
|
||||
const entityId = value || "";
|
||||
|
||||
const extraOption = this._findExtraOption(entityId);
|
||||
if (extraOption) {
|
||||
return html`
|
||||
${this._renderExtraOptionStart(extraOption)}
|
||||
<span slot="headline">${extraOption.primary}</span>
|
||||
${extraOption.secondary
|
||||
? html`<span slot="supporting-text">${extraOption.secondary}</span>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[entityId];
|
||||
|
||||
if (!stateObj) {
|
||||
@@ -141,22 +210,11 @@ export class HaEntityPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
this.hass,
|
||||
stateObj
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return html`
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
@@ -253,8 +311,8 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne(getEntities);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getEntitiesMemoized(
|
||||
private _getItems = () => {
|
||||
const items = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
@@ -265,6 +323,19 @@ export class HaEntityPicker extends LitElement {
|
||||
this.excludeEntities,
|
||||
this.value
|
||||
);
|
||||
if (this.extraOptions?.length) {
|
||||
const resolvedExtras = this.extraOptions.map((opt) => ({
|
||||
...opt,
|
||||
stateObj: opt.entity_id ? this.hass.states[opt.entity_id] : undefined,
|
||||
}));
|
||||
return [...resolvedExtras, ...items];
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
private _shouldHideClearIcon() {
|
||||
return !!this._findExtraOption(this.value)?.hide_clear;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const placeholder =
|
||||
@@ -287,13 +358,14 @@ export class HaEntityPicker extends LitElement {
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.hideClearIcon=${this.hideClearIcon || this._shouldHideClearIcon()}
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.searchKeys=${entityComboBoxKeys}
|
||||
use-top-label
|
||||
.addButtonLabel=${this.addButton
|
||||
? this.hass.localize("ui.components.entity.entity-picker.add")
|
||||
? (this.addButtonLabel ??
|
||||
this.hass.localize("ui.components.entity.entity-picker.add"))
|
||||
: undefined}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.unknown"
|
||||
@@ -341,13 +413,19 @@ export class HaEntityPicker extends LitElement {
|
||||
showHelperDetailDialog(this, {
|
||||
domain,
|
||||
dialogClosedCallback: (item) => {
|
||||
if (item.entityId) this._setValue(item.entityId);
|
||||
if (item.entityId) {
|
||||
if (this.hass.states[item.entityId]) {
|
||||
this._setValue(item.entityId);
|
||||
} else {
|
||||
this._pendingEntityId = item.entityId;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidEntityId(value)) {
|
||||
if (!isValidEntityId(value) && !this._findExtraOption(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ export class HaEntityStatePicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: "no-entity", type: Boolean }) public noEntity = false;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
@@ -122,12 +120,13 @@ export class HaEntityStatePicker extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const noEntity = !ensureArray(this.entityId)?.length;
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.disabled=${this.disabled ||
|
||||
(!this.entityId && this.noEntity === false)}
|
||||
.disabled=${this.disabled || noEntity}
|
||||
.autofocus=${this.autofocus}
|
||||
.required=${this.required}
|
||||
.label=${this.label ??
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
} from "../../data/entity/entity";
|
||||
import { forwardHaptic } from "../../data/haptics";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-control-switch";
|
||||
import "../ha-formfield";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-switch";
|
||||
|
||||
const isOn = (stateObj?: HassEntity) =>
|
||||
stateObj !== undefined &&
|
||||
@@ -35,7 +35,7 @@ export class HaEntityToggle extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.stateObj) {
|
||||
return html` <ha-switch disabled></ha-switch> `;
|
||||
return html`<ha-control-switch disabled></ha-control-switch> `;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -62,14 +62,14 @@ export class HaEntityToggle extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const switchTemplate = html`<ha-switch
|
||||
const switchTemplate = html`<ha-control-switch
|
||||
aria-label=${`Toggle ${computeStateName(this.stateObj)} ${
|
||||
this._isOn ? "off" : "on"
|
||||
}`}
|
||||
.checked=${this._isOn}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@change=${this._toggleChanged}
|
||||
></ha-switch>`;
|
||||
></ha-control-switch>`;
|
||||
|
||||
if (!this.label) {
|
||||
return switchTemplate;
|
||||
@@ -163,6 +163,11 @@ export class HaEntityToggle extends LitElement {
|
||||
white-space: nowrap;
|
||||
min-width: 38px;
|
||||
}
|
||||
ha-control-switch {
|
||||
--control-switch-thickness: 20px;
|
||||
--control-switch-off-color: var(--state-inactive-color);
|
||||
--control-switch-touch-area-size: var(--ha-space-2);
|
||||
}
|
||||
ha-icon-button {
|
||||
--ha-icon-button-size: 40px;
|
||||
color: var(--ha-icon-button-inactive-color, var(--primary-text-color));
|
||||
@@ -171,9 +176,6 @@ export class HaEntityToggle extends LitElement {
|
||||
ha-icon-button.state-active {
|
||||
color: var(--ha-icon-button-active-color, var(--primary-color));
|
||||
}
|
||||
ha-switch {
|
||||
padding: 13px 5px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,6 @@ export class HaAnsiToHtml extends LitElement {
|
||||
lineDiv.appendChild(span);
|
||||
};
|
||||
|
||||
/* eslint-disable no-cond-assign */
|
||||
let match;
|
||||
|
||||
while ((match = re.exec(line)) !== null) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
@@ -85,6 +85,20 @@ export class HaAreaPicker extends LitElement {
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@state() private _pendingAreaId?: string;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (
|
||||
this._pendingAreaId &&
|
||||
changedProperties.has("hass") &&
|
||||
this.hass.areas !== changedProperties.get("hass")?.areas &&
|
||||
this.hass.areas[this._pendingAreaId]
|
||||
) {
|
||||
this._setValue(this._pendingAreaId);
|
||||
this._pendingAreaId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
@@ -243,7 +257,11 @@ export class HaAreaPicker extends LitElement {
|
||||
createEntry: async (values) => {
|
||||
try {
|
||||
const area = await createAreaRegistryEntry(this.hass, values);
|
||||
this._setValue(area.area_id);
|
||||
if (this.hass.areas[area.area_id]) {
|
||||
this._setValue(area.area_id);
|
||||
} else {
|
||||
this._pendingAreaId = area.area_id;
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
|
||||
@@ -26,7 +26,7 @@ class HaAttributeValue extends LitElement {
|
||||
try {
|
||||
// If invalid URL, exception will be raised
|
||||
const url = new URL(attributeValue);
|
||||
if (url.protocol === "http:" || url.protocol === "https:")
|
||||
if (url.protocol === "http:" || url.protocol === "https:") {
|
||||
return html`
|
||||
<a
|
||||
target="_blank"
|
||||
@@ -36,6 +36,7 @@ class HaAttributeValue extends LitElement {
|
||||
${attributeValue}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
} catch {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
@@ -91,6 +91,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
@property({ type: Boolean }) public error = false;
|
||||
|
||||
@property({ type: Boolean }) public lint = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "disable-fullscreen" })
|
||||
public disableFullscreen = false;
|
||||
|
||||
@@ -159,6 +161,40 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
return !!this.renderRoot.querySelector(`span.${className}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a YAML parse error (or null to clear) into the lint gutter as a
|
||||
* diagnostic. Avoids re-parsing the document — the caller (ha-yaml-editor)
|
||||
* already has the error from its own js-yaml load() call.
|
||||
*/
|
||||
public setYamlError(
|
||||
err: {
|
||||
mark?: { position: number; line: number; column: number };
|
||||
reason?: string;
|
||||
} | null
|
||||
): void {
|
||||
if (!this.codemirror || !this._loadedCodeMirror) return;
|
||||
let diagnostics: {
|
||||
from: number;
|
||||
to: number;
|
||||
severity: "error";
|
||||
message: string;
|
||||
}[] = [];
|
||||
if (err) {
|
||||
const doc = this.codemirror.state.doc;
|
||||
const pos = err.mark ? Math.min(err.mark.position, doc.length) : 0;
|
||||
const line = doc.lineAt(pos);
|
||||
const message = `${
|
||||
err.reason ||
|
||||
this.hass?.localize("ui.components.yaml-editor.error") ||
|
||||
"YAML syntax error"
|
||||
}${err.mark ? ` (${this.hass?.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
|
||||
diagnostics = [{ from: pos, to: line.to, severity: "error", message }];
|
||||
}
|
||||
this.codemirror.dispatch(
|
||||
this._loadedCodeMirror.setDiagnostics(this.codemirror.state, diagnostics)
|
||||
);
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.classList.toggle("in-dialog", this.inDialog);
|
||||
@@ -216,17 +252,38 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
transactions.push({
|
||||
effects: [
|
||||
this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode),
|
||||
this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
|
||||
this.lint && !this.readOnly
|
||||
? [this._loadedCodeMirror!.lintGutter()]
|
||||
: []
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
if (changedProps.has("readOnly")) {
|
||||
transactions.push({
|
||||
effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
|
||||
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
|
||||
),
|
||||
effects: [
|
||||
this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
|
||||
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
|
||||
),
|
||||
this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
|
||||
this.lint && !this.readOnly
|
||||
? [this._loadedCodeMirror!.lintGutter()]
|
||||
: []
|
||||
),
|
||||
],
|
||||
});
|
||||
this._updateToolbarButtons();
|
||||
}
|
||||
if (changedProps.has("lint")) {
|
||||
transactions.push({
|
||||
effects: this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
|
||||
this.lint && !this.readOnly
|
||||
? [this._loadedCodeMirror!.lintGutter()]
|
||||
: []
|
||||
),
|
||||
});
|
||||
}
|
||||
if (changedProps.has("linewrap")) {
|
||||
transactions.push({
|
||||
effects: this._loadedCodeMirror!.linewrapCompartment!.reconfigure(
|
||||
@@ -308,6 +365,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
...this._loadedCodeMirror.searchKeymap,
|
||||
...this._loadedCodeMirror.historyKeymap,
|
||||
...this._loadedCodeMirror.tabKeyBindings,
|
||||
...this._loadedCodeMirror.lintKeymap,
|
||||
saveKeyBinding,
|
||||
]),
|
||||
this._loadedCodeMirror.search({ top: true }),
|
||||
@@ -322,6 +380,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror.linewrapCompartment.of(
|
||||
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
|
||||
),
|
||||
this._loadedCodeMirror.yamlLintCompartment.of(
|
||||
this.lint && !this.readOnly ? [this._loadedCodeMirror.lintGutter()] : []
|
||||
),
|
||||
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
|
||||
this._loadedCodeMirror.tooltips({
|
||||
position: "absolute",
|
||||
@@ -370,11 +431,12 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
|
||||
private _fullscreenLabel(): string {
|
||||
if (this._isFullscreen)
|
||||
if (this._isFullscreen) {
|
||||
return (
|
||||
this.hass?.localize("ui.components.yaml-editor.exit_fullscreen") ||
|
||||
"Exit fullscreen"
|
||||
);
|
||||
}
|
||||
return (
|
||||
this.hass?.localize("ui.components.yaml-editor.enter_fullscreen") ||
|
||||
"Enter fullscreen"
|
||||
@@ -914,7 +976,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
// In both cases the parent is a MemberExpression.
|
||||
const memberNode = node.parent;
|
||||
// "from" for the completion result (start of what the user is currently typing)
|
||||
let completionFrom = pos;
|
||||
let completionFrom: number;
|
||||
|
||||
if (
|
||||
node.name === "PropertyName" &&
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
} from "@egjs/hammerjs";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-control-switch")
|
||||
@@ -39,7 +40,7 @@ export class HaControlSwitch extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.setupListeners();
|
||||
this.setupSwipeListeners();
|
||||
}
|
||||
|
||||
private _toggle() {
|
||||
@@ -50,7 +51,19 @@ export class HaControlSwitch extends LitElement {
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.setupListeners();
|
||||
this.setupSwipeListeners();
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("disabled") ||
|
||||
changedProperties.has("vertical") ||
|
||||
changedProperties.has("reversed")
|
||||
) {
|
||||
this.destroyListeners();
|
||||
this.setupSwipeListeners();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
@@ -58,12 +71,13 @@ export class HaControlSwitch extends LitElement {
|
||||
this.destroyListeners();
|
||||
}
|
||||
|
||||
@query("#switch")
|
||||
private switch!: HTMLDivElement;
|
||||
setupSwipeListeners() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
if (this.switch && !this._mc) {
|
||||
this._mc = new Manager(this.switch, {
|
||||
if (!this._mc) {
|
||||
this._mc = new Manager(this, {
|
||||
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
|
||||
});
|
||||
this._mc.add(
|
||||
@@ -90,13 +104,15 @@ export class HaControlSwitch extends LitElement {
|
||||
} else {
|
||||
this._mc.on("swiperight", () => {
|
||||
if (this.disabled) return;
|
||||
this.checked = !this.reversed;
|
||||
const isRTL = mainWindow.document.dir === "rtl";
|
||||
this.checked = (!this.reversed && !isRTL) || (this.reversed && isRTL);
|
||||
fireEvent(this, "change");
|
||||
});
|
||||
|
||||
this._mc.on("swipeleft", () => {
|
||||
if (this.disabled) return;
|
||||
this.checked = !!this.reversed;
|
||||
const isRTL = mainWindow.document.dir === "rtl";
|
||||
this.checked = (this.reversed && !isRTL) || (!this.reversed && isRTL);
|
||||
fireEvent(this, "change");
|
||||
});
|
||||
}
|
||||
@@ -116,11 +132,30 @@ export class HaControlSwitch extends LitElement {
|
||||
}
|
||||
|
||||
private _keydown(ev: any) {
|
||||
if (ev.key !== "Enter" && ev.key !== " ") {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._toggle();
|
||||
return;
|
||||
}
|
||||
|
||||
const rtl = !this.vertical && mainWindow.document.dir === "rtl";
|
||||
const flip = this.reversed !== rtl;
|
||||
const [forward, backward] = this.vertical
|
||||
? ["ArrowDown", "ArrowUp"]
|
||||
: ["ArrowRight", "ArrowLeft"];
|
||||
const onKey = flip ? backward : forward;
|
||||
const offKey = flip ? forward : backward;
|
||||
|
||||
if (ev.key !== onKey && ev.key !== offKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
this._toggle();
|
||||
|
||||
const wantOn = ev.key === onKey;
|
||||
if (wantOn !== this.checked) {
|
||||
this._toggle();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -132,7 +167,7 @@ export class HaControlSwitch extends LitElement {
|
||||
aria-checked=${this.checked ? "true" : "false"}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
role="switch"
|
||||
tabindex="0"
|
||||
tabindex=${ifDefined(this.disabled ? undefined : "0")}
|
||||
?checked=${this.checked}
|
||||
?disabled=${this.disabled}
|
||||
>
|
||||
@@ -153,12 +188,15 @@ export class HaControlSwitch extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
--control-switch-on-color: var(--primary-color);
|
||||
--control-switch-off-color: var(--disabled-color);
|
||||
--control-switch-background-opacity: 0.2;
|
||||
--control-switch-hover-background-opacity: 0.4;
|
||||
--control-switch-thickness: 40px;
|
||||
--control-switch-border-radius: var(--ha-border-radius-lg);
|
||||
--control-switch-padding: 4px;
|
||||
--control-switch-touch-area-size: 0px;
|
||||
--mdc-icon-size: 20px;
|
||||
height: var(--control-switch-thickness);
|
||||
width: 100%;
|
||||
@@ -167,10 +205,15 @@ export class HaControlSwitch extends LitElement {
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.switch:focus-visible {
|
||||
:host::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: calc(-1 * var(--control-switch-touch-area-size));
|
||||
}
|
||||
.switch:not([disabled]):focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-switch-off-color);
|
||||
}
|
||||
.switch[checked]:focus-visible {
|
||||
.switch[checked]:not([disabled]):focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-switch-on-color);
|
||||
}
|
||||
.switch {
|
||||
@@ -199,6 +242,10 @@ export class HaControlSwitch extends LitElement {
|
||||
transition: background-color 180ms ease-in-out;
|
||||
opacity: var(--control-switch-background-opacity);
|
||||
}
|
||||
.switch:not([disabled]):focus-visible .background,
|
||||
.switch:not([disabled]):hover .background {
|
||||
opacity: var(--control-switch-hover-background-opacity);
|
||||
}
|
||||
.switch .button {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
@@ -222,12 +269,19 @@ export class HaControlSwitch extends LitElement {
|
||||
transform: translateX(100%);
|
||||
background-color: var(--control-switch-on-color);
|
||||
}
|
||||
.switch[checked] .button:dir(rtl) {
|
||||
transform: translateX(-100%);
|
||||
background-color: var(--control-switch-on-color);
|
||||
}
|
||||
:host([reversed]) .switch {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
:host([reversed]) .switch[checked] .button {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
:host([reversed]) .switch[checked] .button:dir(rtl) {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--control-switch-thickness);
|
||||
height: 100%;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
@@ -104,6 +104,20 @@ export class HaFloorPicker extends LitElement {
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@state() private _pendingFloorId?: string;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (
|
||||
this._pendingFloorId &&
|
||||
changedProperties.has("hass") &&
|
||||
this.hass.floors !== changedProperties.get("hass")?.floors &&
|
||||
this.hass.floors[this._pendingFloorId]
|
||||
) {
|
||||
this._setValue(this._pendingFloorId);
|
||||
this._pendingFloorId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
@@ -436,7 +450,11 @@ export class HaFloorPicker extends LitElement {
|
||||
floor_id: floor.floor_id,
|
||||
});
|
||||
});
|
||||
this._setValue(floor.floor_id);
|
||||
if (this.hass.floors[floor.floor_id]) {
|
||||
this._setValue(floor.floor_id);
|
||||
} else {
|
||||
this._pendingFloorId = floor.floor_id;
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
|
||||
@@ -72,6 +72,8 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public context?: Record<string, any>;
|
||||
|
||||
protected getFormProperties(): Record<string, any> {
|
||||
return {};
|
||||
}
|
||||
@@ -218,13 +220,15 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
private _generateContext(
|
||||
schema: HaFormSchema
|
||||
): Record<string, any> | undefined {
|
||||
if (!schema.context) {
|
||||
if (!schema.context && !this.context) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context = {};
|
||||
for (const [context_key, data_key] of Object.entries(schema.context)) {
|
||||
context[context_key] = this.data[data_key];
|
||||
const context = { ...this.context };
|
||||
if (schema.context) {
|
||||
for (const [context_key, data_key] of Object.entries(schema.context)) {
|
||||
context[context_key] = this.data[data_key];
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -445,10 +445,10 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
}
|
||||
|
||||
wa-popover::part(body) {
|
||||
width: max(var(--body-width), 250px);
|
||||
width: var(--ha-generic-picker-width, max(var(--body-width), 250px));
|
||||
max-width: var(
|
||||
--ha-generic-picker-max-width,
|
||||
max(var(--body-width), 250px)
|
||||
var(--ha-generic-picker-width, max(var(--body-width), 250px))
|
||||
);
|
||||
max-height: 500px;
|
||||
height: 70vh;
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
@@ -117,6 +117,19 @@ export class HaLabelPicker extends LitElement {
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@state() private _pendingLabelId?: string;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (
|
||||
this._pendingLabelId &&
|
||||
changedProperties.has("_labels") &&
|
||||
this._labels?.some((l) => l.label_id === this._pendingLabelId)
|
||||
) {
|
||||
this._setValue(this._pendingLabelId);
|
||||
this._pendingLabelId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
@@ -248,7 +261,11 @@ export class HaLabelPicker extends LitElement {
|
||||
createEntry: async (values) => {
|
||||
try {
|
||||
const label = await createLabelRegistryEntry(this.hass, values);
|
||||
this._setValue(label.label_id);
|
||||
if (this._labels?.some((l) => l.label_id === label.label_id)) {
|
||||
this._setValue(label.label_id);
|
||||
} else {
|
||||
this._pendingLabelId = label.label_id;
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
} from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomCurrentTargetEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
@@ -52,6 +55,7 @@ export interface PickerComboBoxItem {
|
||||
id: string;
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
disabled?: boolean;
|
||||
search_labels?: Record<string, string | null>;
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
@@ -64,6 +68,12 @@ export interface PickerComboBoxIndexSelectedDetail {
|
||||
newTab?: boolean;
|
||||
}
|
||||
|
||||
type PickerComboBoxRowElement = HTMLDivElement & {
|
||||
disabled?: boolean;
|
||||
index: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
||||
const PADDING_ID = "___padding___";
|
||||
|
||||
@@ -425,6 +435,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
class="combo-box-row ${this.value === item.id ? "current-value" : ""}"
|
||||
.value=${item.id}
|
||||
.index=${index}
|
||||
.disabled=${item.disabled}
|
||||
@click=${this._valueSelected}
|
||||
>
|
||||
${renderer(item, index)}
|
||||
@@ -437,10 +448,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._listScrolled = top > 0;
|
||||
}
|
||||
|
||||
private _valueSelected = (ev: MouseEvent) => {
|
||||
private _valueSelected = (
|
||||
ev: MouseEvent & HASSDomCurrentTargetEvent<PickerComboBoxRowElement>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
const value = (ev.currentTarget as any).value as string;
|
||||
const index = Number((ev.currentTarget as any).index);
|
||||
const { disabled, index, value } = ev.currentTarget;
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
const newValue = value?.trim();
|
||||
const newTab = ev.ctrlKey || ev.metaKey;
|
||||
|
||||
@@ -728,7 +743,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
(
|
||||
this.virtualizerElement?.items as (PickerComboBoxItem | string)[]
|
||||
).forEach((item, index) => {
|
||||
if (typeof item !== "string") {
|
||||
if (typeof item !== "string" && !item.disabled) {
|
||||
this._fireSelectedEvents(item.id, index, newTab);
|
||||
}
|
||||
});
|
||||
@@ -748,7 +763,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
const item = this.virtualizerElement?.items[
|
||||
this._selectedItemIndex
|
||||
] as PickerComboBoxItem;
|
||||
if (item) {
|
||||
if (item && !item.disabled) {
|
||||
this._fireSelectedEvents(item.id, this._selectedItemIndex, newTab);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,6 +36,9 @@ export class HaSelectBox extends LitElement {
|
||||
@property({ type: Number, attribute: "max_columns" })
|
||||
public maxColumns?: number;
|
||||
|
||||
@property({ type: Boolean, attribute: "stacked_image" })
|
||||
public stackedImage = false;
|
||||
|
||||
render() {
|
||||
const maxColumns = this.maxColumns ?? 3;
|
||||
const columns = Math.min(maxColumns, this.options.length);
|
||||
@@ -48,7 +51,8 @@ export class HaSelectBox extends LitElement {
|
||||
}
|
||||
|
||||
private _renderOption(option: SelectBoxOption) {
|
||||
const horizontal = this.maxColumns === 1;
|
||||
const horizontal = this.maxColumns === 1 && !this.stackedImage;
|
||||
const stacked = this.maxColumns === 1 && this.stackedImage;
|
||||
const disabled = option.disabled || this.disabled || false;
|
||||
const selected = option.value === this.value;
|
||||
|
||||
@@ -66,6 +70,7 @@ export class HaSelectBox extends LitElement {
|
||||
<label
|
||||
class="option ${classMap({
|
||||
horizontal: horizontal,
|
||||
stacked: stacked,
|
||||
selected: selected,
|
||||
})}"
|
||||
?disabled=${disabled}
|
||||
@@ -187,6 +192,16 @@ export class HaSelectBox extends LitElement {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.option.stacked {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.option.stacked img {
|
||||
max-width: 100%;
|
||||
max-height: var(--ha-select-box-image-size, 96px);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.option:before {
|
||||
content: "";
|
||||
display: block;
|
||||
|
||||
@@ -59,7 +59,7 @@ export class HaSelect extends LitElement {
|
||||
value: string | number | undefined
|
||||
) => {
|
||||
// just in case value is a number, convert it to string to avoid falsy value
|
||||
const valueStr = String(value);
|
||||
const valueStr = value !== undefined ? String(value) : undefined;
|
||||
if (!options || !valueStr) {
|
||||
return valueStr;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export class HaSelectorAttribute extends LitElement {
|
||||
}
|
||||
|
||||
// Validate that that the attribute is still valid for this entity, else unselect.
|
||||
let invalid = false;
|
||||
let invalid: boolean;
|
||||
if (this.context.filter_entity) {
|
||||
const entityIds = ensureArray(this.context.filter_entity);
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import type {
|
||||
AutomationBehavior,
|
||||
AutomationBehaviorConditionMode,
|
||||
AutomationBehaviorSelector,
|
||||
AutomationBehaviorTriggerMode,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import type { SelectBoxOption } from "../ha-select-box";
|
||||
import "../ha-select-box";
|
||||
|
||||
const TRIGGER_BEHAVIORS: AutomationBehaviorTriggerMode[] = [
|
||||
"any",
|
||||
"first",
|
||||
"last",
|
||||
];
|
||||
|
||||
const CONDITION_BEHAVIORS: AutomationBehaviorConditionMode[] = ["any", "all"];
|
||||
|
||||
@customElement("ha-selector-automation_behavior")
|
||||
export class HaSelectorAutomationBehavior extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public selector!: AutomationBehaviorSelector;
|
||||
|
||||
@property() public value?: AutomationBehavior;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public localizeValue?: (key: string) => string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
const { mode } = this.selector.automation_behavior ?? {};
|
||||
const modeKey = mode ?? "trigger";
|
||||
|
||||
const isTrigger = modeKey === "trigger";
|
||||
|
||||
const options = this._behaviors().map<SelectBoxOption>((behavior) => ({
|
||||
value: behavior,
|
||||
label: this._localizeOption(behavior, "label"),
|
||||
description: this._localizeOption(behavior, "description"),
|
||||
disabled: this.disabled,
|
||||
...(isTrigger && {
|
||||
image: {
|
||||
src: `/static/images/form/automation_behavior_trigger_${behavior}.svg`,
|
||||
src_dark: `/static/images/form/automation_behavior_trigger_${behavior}_dark.svg`,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
return html`
|
||||
<ha-select-box
|
||||
.hass=${this.hass}
|
||||
.options=${options}
|
||||
.value=${this.value ?? ""}
|
||||
max_columns="1"
|
||||
?stacked_image=${isTrigger}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-select-box>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _behaviors(): AutomationBehavior[] {
|
||||
const mode = this.selector.automation_behavior?.mode;
|
||||
return mode === "condition" ? CONDITION_BEHAVIORS : TRIGGER_BEHAVIORS;
|
||||
}
|
||||
|
||||
private _localizeOption(
|
||||
behavior: AutomationBehavior,
|
||||
field: "label" | "description"
|
||||
): string {
|
||||
const { translation_key: translationKey, mode } =
|
||||
this.selector.automation_behavior ?? {};
|
||||
|
||||
if (this.localizeValue && translationKey) {
|
||||
const translated = this.localizeValue(
|
||||
`${translationKey}.options.${behavior}.${field}`
|
||||
);
|
||||
if (translated) {
|
||||
return translated;
|
||||
}
|
||||
}
|
||||
return this.hass.localize(
|
||||
`ui.components.selectors.automation_behavior.${mode ?? "trigger"}.options.${behavior}.${field}` as LocalizeKeys
|
||||
);
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as AutomationBehavior;
|
||||
if (this.disabled || value === this.value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select-box {
|
||||
--ha-select-box-image-size: 28px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-automation_behavior": HaSelectorAutomationBehavior;
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ export class HaEntitySelector extends LitElement {
|
||||
.helper=${this.helper}
|
||||
.includeEntities=${this.selector.entity?.include_entities}
|
||||
.excludeEntities=${this.selector.entity?.exclude_entities}
|
||||
.extraOptions=${this.selector.entity?.extra_options}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.createDomains=${this._createDomains}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiClose, mdiConnection, mdiMemory, mdiPencil, mdiUsb } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -27,21 +29,32 @@ const MANUAL_ENTRY_ID = "__manual_entry__";
|
||||
const SERIAL_PORTS_REFRESH_INTERVAL = 5000;
|
||||
|
||||
type SerialPortType =
|
||||
| "recommended"
|
||||
| "serial_proxy"
|
||||
| "integration"
|
||||
| "usb"
|
||||
| "embedded"
|
||||
| "unnamed"
|
||||
| "not_recommended";
|
||||
|
||||
const SECTION_ORDER: SerialPortType[] = [
|
||||
"recommended",
|
||||
"serial_proxy",
|
||||
"integration",
|
||||
"usb",
|
||||
"embedded",
|
||||
"unnamed",
|
||||
"not_recommended",
|
||||
];
|
||||
|
||||
type BaseSerialPortType =
|
||||
| "serial_proxy"
|
||||
| "integration"
|
||||
| "usb"
|
||||
| "embedded"
|
||||
| "unnamed";
|
||||
|
||||
const SECTION_ORDER: SerialPortType[] = [
|
||||
"serial_proxy",
|
||||
"integration",
|
||||
"usb",
|
||||
"embedded",
|
||||
"unnamed",
|
||||
];
|
||||
|
||||
const TYPE_ICONS: Record<SerialPortType, string> = {
|
||||
const TYPE_ICONS: Record<BaseSerialPortType, string> = {
|
||||
serial_proxy: mdiEsphomeLogo,
|
||||
integration: mdiConnection,
|
||||
usb: mdiUsb,
|
||||
@@ -51,7 +64,7 @@ const TYPE_ICONS: Record<SerialPortType, string> = {
|
||||
|
||||
const ESPHOME_HASS_SCHEME = "esphome-hass://";
|
||||
|
||||
const getPortType = (port: SerialPort): SerialPortType => {
|
||||
const getBasePortType = (port: SerialPort): BaseSerialPortType => {
|
||||
if (port.device.startsWith(ESPHOME_HASS_SCHEME)) {
|
||||
return "serial_proxy";
|
||||
}
|
||||
@@ -67,6 +80,37 @@ const getPortType = (port: SerialPort): SerialPortType => {
|
||||
return "unnamed";
|
||||
};
|
||||
|
||||
interface SerialPickerItem extends PickerComboBoxItem {
|
||||
port_type: SerialPortType;
|
||||
used_by?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const integrationName = (
|
||||
localize: HomeAssistant["localize"],
|
||||
domain: string
|
||||
): string => localize(`component.${domain}.title`) || domain;
|
||||
|
||||
const getPortType = (
|
||||
port: SerialPort,
|
||||
recommendedDomains: Set<string>
|
||||
): SerialPortType => {
|
||||
const matchingDomains = port.matching_integrations ?? [];
|
||||
|
||||
// If the current integration matches this port, it is recommended
|
||||
if (matchingDomains.some((d) => recommendedDomains.has(d))) {
|
||||
return "recommended";
|
||||
}
|
||||
|
||||
// If any other integrations match it, the port is not recommended
|
||||
if (recommendedDomains.size > 0 && matchingDomains.length > 0) {
|
||||
return "not_recommended";
|
||||
}
|
||||
|
||||
// Otherwise, classify the port
|
||||
return getBasePortType(port);
|
||||
};
|
||||
|
||||
@customElement("ha-selector-serial_port")
|
||||
export class HaSerialPortSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -85,6 +129,8 @@ export class HaSerialPortSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ attribute: false }) public context?: Record<string, any>;
|
||||
|
||||
@state() private _serialPorts?: SerialPort[];
|
||||
|
||||
@state() private _manualEntry = false;
|
||||
@@ -172,24 +218,29 @@ export class HaSerialPortSelector extends LitElement {
|
||||
language: string,
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
localize: HomeAssistant["localize"]
|
||||
): Record<SerialPortType, PickerComboBoxItem[]> => {
|
||||
const grouped: Record<SerialPortType, PickerComboBoxItem[]> = {
|
||||
localize: HomeAssistant["localize"],
|
||||
recommendedDomains: Set<string>
|
||||
): Record<SerialPortType, SerialPickerItem[]> => {
|
||||
const grouped: Record<SerialPortType, SerialPickerItem[]> = {
|
||||
recommended: [],
|
||||
serial_proxy: [],
|
||||
integration: [],
|
||||
usb: [],
|
||||
embedded: [],
|
||||
unnamed: [],
|
||||
not_recommended: [],
|
||||
};
|
||||
|
||||
for (const port of ports) {
|
||||
const type = getPortType(port);
|
||||
const type = getPortType(port, recommendedDomains);
|
||||
let primary: string;
|
||||
let description: string | undefined;
|
||||
let secondary: string | undefined;
|
||||
const searchLabels: Record<string, string | null> = {
|
||||
device: port.device,
|
||||
manufacturer: port.manufacturer,
|
||||
description: port.description,
|
||||
interface_description: port.interface_description ?? null,
|
||||
serial_number: port.serial_number,
|
||||
};
|
||||
|
||||
@@ -223,13 +274,25 @@ export class HaSerialPortSelector extends LitElement {
|
||||
searchLabels.port_name = port.device;
|
||||
}
|
||||
} else {
|
||||
primary =
|
||||
const productManufacturer =
|
||||
port.description && port.manufacturer
|
||||
? `${port.description} — ${port.manufacturer}`
|
||||
: port.description || port.manufacturer || port.device;
|
||||
: port.description || port.manufacturer;
|
||||
|
||||
// Prefer the interface description if one exists
|
||||
if (
|
||||
port.interface_description &&
|
||||
port.interface_description !== port.description
|
||||
) {
|
||||
primary = port.interface_description;
|
||||
description = productManufacturer || undefined;
|
||||
} else {
|
||||
primary = productManufacturer || port.device;
|
||||
description = undefined;
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
if (port.description || port.manufacturer) {
|
||||
if (primary !== port.device) {
|
||||
parts.push(port.device);
|
||||
}
|
||||
if (port.vid && port.pid) {
|
||||
@@ -238,16 +301,31 @@ export class HaSerialPortSelector extends LitElement {
|
||||
if (port.serial_number) {
|
||||
parts.push(`S/N: ${port.serial_number}`);
|
||||
}
|
||||
secondary = parts.length ? parts.join(" · ") : undefined;
|
||||
|
||||
secondary = parts.join(" · ");
|
||||
}
|
||||
|
||||
let used_by: string | undefined;
|
||||
if (type === "not_recommended" && port.matching_integrations.length) {
|
||||
const integrations = port.matching_integrations
|
||||
.map((d) => integrationName(localize, d))
|
||||
.join(", ");
|
||||
used_by = localize("ui.components.selectors.serial_port.used_by", {
|
||||
integrations,
|
||||
});
|
||||
searchLabels.used_by = used_by;
|
||||
}
|
||||
|
||||
grouped[type].push({
|
||||
id: port.device,
|
||||
primary,
|
||||
secondary,
|
||||
icon_path: TYPE_ICONS[type],
|
||||
icon_path: TYPE_ICONS[getBasePortType(port)],
|
||||
search_labels: searchLabels,
|
||||
sorting_label: primary,
|
||||
port_type: type,
|
||||
used_by,
|
||||
description: description,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -265,6 +343,42 @@ export class HaSerialPortSelector extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _sectionLabel(type: SerialPortType): string {
|
||||
const key = `ui.components.selectors.serial_port.type.${type}` as const;
|
||||
if (type === "recommended" && this._selectorDomain) {
|
||||
return this.hass.localize(key, {
|
||||
integration: integrationName(this.hass.localize, this._selectorDomain),
|
||||
});
|
||||
}
|
||||
return this.hass.localize(key);
|
||||
}
|
||||
|
||||
private get _selectorDomain(): string | undefined {
|
||||
return this.context?.handler;
|
||||
}
|
||||
|
||||
private _memoRecommendedDomains = memoizeOne(
|
||||
(domain: string | undefined, extra: string[] | undefined): Set<string> => {
|
||||
const domains = new Set<string>();
|
||||
if (domain) {
|
||||
domains.add(domain);
|
||||
}
|
||||
if (extra) {
|
||||
for (const d of extra) {
|
||||
domains.add(d);
|
||||
}
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
);
|
||||
|
||||
private get _recommendedDomains(): Set<string> {
|
||||
return this._memoRecommendedDomains(
|
||||
this._selectorDomain,
|
||||
this.selector?.serial_port?.extra_recommended_domains
|
||||
);
|
||||
}
|
||||
|
||||
private _getPickerItems = (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
@@ -278,7 +392,8 @@ export class HaSerialPortSelector extends LitElement {
|
||||
this.hass.locale.language,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.localize
|
||||
this.hass.localize,
|
||||
this._recommendedDomains
|
||||
);
|
||||
|
||||
const items: (PickerComboBoxItem | string)[] = [];
|
||||
@@ -286,7 +401,7 @@ export class HaSerialPortSelector extends LitElement {
|
||||
if (section && section !== type) {
|
||||
continue;
|
||||
}
|
||||
let groupItems = grouped[type];
|
||||
let groupItems: SerialPickerItem[] = grouped[type];
|
||||
if (searchString) {
|
||||
groupItems = multiTermSortedSearch(
|
||||
groupItems,
|
||||
@@ -299,11 +414,7 @@ export class HaSerialPortSelector extends LitElement {
|
||||
continue;
|
||||
}
|
||||
if (!section) {
|
||||
items.push(
|
||||
this.hass.localize(
|
||||
`ui.components.selectors.serial_port.type.${type}` as const
|
||||
)
|
||||
);
|
||||
items.push(this._sectionLabel(type));
|
||||
}
|
||||
items.push(...groupItems);
|
||||
}
|
||||
@@ -321,17 +432,48 @@ export class HaSerialPortSelector extends LitElement {
|
||||
},
|
||||
];
|
||||
|
||||
private _rowRenderer = (item: PickerComboBoxItem) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
${item.icon_path
|
||||
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
|
||||
: nothing}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
private _rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => {
|
||||
const manual = item.id === MANUAL_ENTRY_ID;
|
||||
const { port_type, used_by, description } = item as SerialPickerItem;
|
||||
return html`
|
||||
<ha-combo-box-item
|
||||
type="button"
|
||||
compact
|
||||
.borderTop=${manual}
|
||||
style=${styleMap({
|
||||
marginTop: manual ? "var(--ha-space-3)" : "",
|
||||
opacity: port_type === "not_recommended" ? "0.6" : "",
|
||||
backgroundColor:
|
||||
port_type === "recommended"
|
||||
? "var(--ha-assist-chip-active-container-color)"
|
||||
: "",
|
||||
})}
|
||||
>
|
||||
${item.icon_path
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<span slot="headline" style="white-space: normal">${item.primary}</span>
|
||||
${used_by
|
||||
? html`<span slot="supporting-text" style="white-space: normal"
|
||||
>${used_by}</span
|
||||
>`
|
||||
: nothing}
|
||||
${description
|
||||
? html`<span slot="supporting-text" style="white-space: normal"
|
||||
>${description}</span
|
||||
>`
|
||||
: nothing}
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text" style="white-space: normal"
|
||||
>${item.secondary}</span
|
||||
>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
const usbLoaded = this.hass && isComponentLoaded(this.hass.config, "usb");
|
||||
@@ -393,7 +535,8 @@ export class HaSerialPortSelector extends LitElement {
|
||||
this.hass.locale.language,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.localize
|
||||
this.hass.localize,
|
||||
this._recommendedDomains
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
@@ -415,13 +558,12 @@ export class HaSerialPortSelector extends LitElement {
|
||||
this.hass.locale.language,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.localize
|
||||
this.hass.localize,
|
||||
this._recommendedDomains
|
||||
);
|
||||
return SECTION_ORDER.filter((type) => grouped[type].length).map((type) => ({
|
||||
id: type,
|
||||
label: this.hass.localize(
|
||||
`ui.components.selectors.serial_port.type.${type}` as const
|
||||
),
|
||||
label: this._sectionLabel(type),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,6 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.noEntity=${this.selector.state?.no_entity ?? false}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-value
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { HomeAssistant } from "../../types";
|
||||
const LOAD_ELEMENTS = {
|
||||
action: () => import("./ha-selector-action"),
|
||||
addon: () => import("./ha-selector-addon"),
|
||||
automation_behavior: () => import("./ha-selector-automation-behavior"),
|
||||
app: () => import("./ha-selector-app"),
|
||||
area: () => import("./ha-selector-area"),
|
||||
areas_display: () => import("./ha-selector-areas-display"),
|
||||
|
||||
@@ -36,7 +36,8 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
import "./ha-fade-in";
|
||||
import { isMobileClient } from "../util/is_mobile";
|
||||
import "./animation/ha-fade-in";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-md-list";
|
||||
@@ -579,6 +580,10 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
private _renderToolTip(id: string, text: string) {
|
||||
if (isMobileClient) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-tooltip
|
||||
for=${id}
|
||||
show-delay="0"
|
||||
|
||||
@@ -157,12 +157,24 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
),
|
||||
};
|
||||
|
||||
@state() private _pendingEntityId?: string;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
|
||||
if (
|
||||
this._pendingEntityId &&
|
||||
changedProps.has("hass") &&
|
||||
this.hass.states !== changedProps.get("hass")?.states &&
|
||||
this.hass.states[this._pendingEntityId]
|
||||
) {
|
||||
this._addTarget(this._pendingEntityId, "entity");
|
||||
this._pendingEntityId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _createFuseIndex = (states, keys: FuseWeightedKey[]) =>
|
||||
@@ -532,10 +544,11 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
domain,
|
||||
dialogClosedCallback: (item) => {
|
||||
if (item.entityId) {
|
||||
// prevent error that new entity_id isn't in hass object
|
||||
requestAnimationFrame(() => {
|
||||
this._addTarget(item.entityId!, "entity");
|
||||
});
|
||||
if (this.hass.states[item.entityId]) {
|
||||
this._addTarget(item.entityId, "entity");
|
||||
} else {
|
||||
this._pendingEntityId = item.entityId;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -234,7 +234,10 @@ export class HaToast extends LitElement {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
opacity: 0;
|
||||
transform: translate(-50%, var(--ha-space-2));
|
||||
transform: translate(
|
||||
calc(-50% * var(--scale-direction)),
|
||||
var(--ha-space-2)
|
||||
);
|
||||
transition:
|
||||
opacity var(--ha-animation-duration-fast, 150ms) ease,
|
||||
transform var(--ha-animation-duration-fast, 150ms) ease;
|
||||
@@ -242,7 +245,7 @@ export class HaToast extends LitElement {
|
||||
|
||||
.toast.visible {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
transform: translate(calc(-50% * var(--scale-direction)), 0);
|
||||
}
|
||||
|
||||
.toast:not(.active) {
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiCheckCircleOutline,
|
||||
mdiChevronDown,
|
||||
mdiHelpCircleOutline,
|
||||
mdiProgressClock,
|
||||
mdiProgressWrench,
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { Trace } from "../data/trace";
|
||||
import "./ha-button";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
|
||||
@customElement("ha-trace-picker")
|
||||
class HaTracePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public traces!: Trace[];
|
||||
|
||||
@property({ attribute: false }) public value?: string;
|
||||
|
||||
@query("ha-generic-picker") private tracePicker?: HaGenericPicker;
|
||||
|
||||
protected render() {
|
||||
return html` <ha-generic-picker
|
||||
name="trace"
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.trace.select_trace"
|
||||
)}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getTraces}
|
||||
required
|
||||
>
|
||||
<ha-button
|
||||
slot="field"
|
||||
appearance="filled"
|
||||
variant="neutral"
|
||||
size="small"
|
||||
@click=${this._openPicker}
|
||||
>
|
||||
${this._renderTracePickerValue(this.value!)}
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronDown}></ha-svg-icon>
|
||||
</ha-button>
|
||||
</ha-generic-picker>`;
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this.tracePicker?.open();
|
||||
}
|
||||
|
||||
private _getTraces = (): PickerComboBoxItem[] =>
|
||||
this.traces?.map((trace) => {
|
||||
const renderRuntime = () =>
|
||||
(
|
||||
(new Date(trace.timestamp.finish!).getTime() -
|
||||
new Date(trace.timestamp.start).getTime()) /
|
||||
1000
|
||||
).toFixed(2);
|
||||
|
||||
const item: PickerComboBoxItem = {
|
||||
id: trace.run_id,
|
||||
primary: formatDateTimeWithSeconds(
|
||||
new Date(trace.timestamp.start),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
};
|
||||
if (trace.state === "running") {
|
||||
item.secondary = this.hass.localize(
|
||||
"ui.panel.config.automation.trace.picker.still_running"
|
||||
);
|
||||
item.icon_path = mdiProgressClock;
|
||||
} else if (trace.state === "debugged") {
|
||||
item.secondary = this.hass.localize(
|
||||
"ui.panel.config.automation.trace.picker.debugged"
|
||||
);
|
||||
item.icon_path = mdiProgressWrench;
|
||||
} else if (trace.script_execution === "finished") {
|
||||
item.secondary = this.hass.localize(
|
||||
"ui.panel.config.automation.trace.picker.finished",
|
||||
{
|
||||
executiontime: renderRuntime(),
|
||||
}
|
||||
);
|
||||
item.icon_path = mdiCheckCircleOutline;
|
||||
} else if (trace.script_execution === "aborted") {
|
||||
item.secondary = this.hass.localize(
|
||||
"ui.panel.config.automation.trace.picker.aborted",
|
||||
{
|
||||
executiontime: renderRuntime(),
|
||||
}
|
||||
);
|
||||
item.icon_path = mdiAlertCircleOutline;
|
||||
} else if (trace.script_execution === "cancelled") {
|
||||
item.secondary = this.hass.localize(
|
||||
"ui.panel.config.automation.trace.picker.cancelled",
|
||||
{
|
||||
executiontime: renderRuntime(),
|
||||
}
|
||||
);
|
||||
item.icon_path = mdiAlertCircleOutline;
|
||||
} else {
|
||||
let message:
|
||||
| "stopped_failed_conditions"
|
||||
| "stopped_failed_single"
|
||||
| "stopped_failed_max_runs"
|
||||
| "stopped_error"
|
||||
| "stopped_unknown_reason";
|
||||
let error: string | undefined;
|
||||
let icon: string;
|
||||
|
||||
switch (trace.script_execution) {
|
||||
case "failed_conditions":
|
||||
message = "stopped_failed_conditions";
|
||||
icon = mdiStopCircleOutline;
|
||||
break;
|
||||
case "failed_single":
|
||||
message = "stopped_failed_single";
|
||||
icon = mdiStopCircleOutline;
|
||||
break;
|
||||
case "failed_max_runs":
|
||||
message = "stopped_failed_max_runs";
|
||||
icon = mdiStopCircleOutline;
|
||||
break;
|
||||
case "error":
|
||||
message = "stopped_error";
|
||||
error = trace.error!;
|
||||
icon = mdiAlertCircleOutline;
|
||||
break;
|
||||
default:
|
||||
message = "stopped_unknown_reason";
|
||||
icon = mdiHelpCircleOutline;
|
||||
}
|
||||
|
||||
item.secondary = this.hass.localize(
|
||||
`ui.panel.config.automation.trace.picker.${message}`,
|
||||
{
|
||||
error,
|
||||
executiontime: renderRuntime(),
|
||||
}
|
||||
);
|
||||
item.icon_path = icon;
|
||||
}
|
||||
return item;
|
||||
}) ?? [];
|
||||
|
||||
private _renderTracePickerValue = (runId: string) => {
|
||||
const trace = this.traces?.find((t) => t.run_id === runId);
|
||||
return html`${trace
|
||||
? formatDateTimeWithSeconds(
|
||||
new Date(trace.timestamp.start),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: runId}`;
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
}
|
||||
ha-button {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-trace-picker": HaTracePicker;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
import "./ha-alert";
|
||||
import "./ha-button";
|
||||
import "./ha-code-editor";
|
||||
import type { HaCodeEditor } from "./ha-code-editor";
|
||||
@@ -58,15 +57,8 @@ export class HaYamlEditor extends LitElement {
|
||||
@property({ attribute: "has-extra-actions", type: Boolean })
|
||||
public hasExtraActions = false;
|
||||
|
||||
@property({ attribute: "show-errors", type: Boolean })
|
||||
public showErrors = true;
|
||||
|
||||
@state() private _yaml = "";
|
||||
|
||||
@state() private _error = "";
|
||||
|
||||
@state() private _showingError = false;
|
||||
|
||||
@query("ha-code-editor") _codeEditor?: HaCodeEditor;
|
||||
|
||||
public setValue(value): void {
|
||||
@@ -126,16 +118,14 @@ export class HaYamlEditor extends LitElement {
|
||||
.disableFullscreen=${this.disableFullscreen}
|
||||
.inDialog=${this.inDialog}
|
||||
mode="yaml"
|
||||
lint
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
.error=${this.isValid === false}
|
||||
@value-changed=${this._onChange}
|
||||
@blur=${this._onBlur}
|
||||
@editor-save=${this._onEditorSave}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
${this._showingError
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
${this.copyClipboard || this.hasExtraActions
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
@@ -158,9 +148,13 @@ export class HaYamlEditor extends LitElement {
|
||||
private _onChange(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
this._yaml = ev.detail.value;
|
||||
let parsed;
|
||||
let parsed: unknown;
|
||||
let isValid = true;
|
||||
let errorMsg;
|
||||
let errorMsg: string | undefined;
|
||||
let yamlError: {
|
||||
mark?: { position: number; line: number; column: number };
|
||||
message?: string;
|
||||
} | null = null;
|
||||
|
||||
if (this._yaml) {
|
||||
try {
|
||||
@@ -168,15 +162,13 @@ export class HaYamlEditor extends LitElement {
|
||||
} catch (err: any) {
|
||||
// Invalid YAML
|
||||
isValid = false;
|
||||
yamlError = err;
|
||||
errorMsg = `${this.hass.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this.hass.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
|
||||
}
|
||||
} else {
|
||||
parsed = {};
|
||||
}
|
||||
this._error = errorMsg ?? "";
|
||||
if (isValid) {
|
||||
this._showingError = false;
|
||||
}
|
||||
this._codeEditor?.setYamlError(yamlError);
|
||||
|
||||
this.value = parsed;
|
||||
this.isValid = isValid;
|
||||
@@ -188,16 +180,23 @@ export class HaYamlEditor extends LitElement {
|
||||
} as any);
|
||||
}
|
||||
|
||||
private _onBlur(): void {
|
||||
if (this.showErrors && this._error) {
|
||||
this._showingError = true;
|
||||
}
|
||||
}
|
||||
|
||||
get yaml() {
|
||||
return this._yaml;
|
||||
}
|
||||
|
||||
get codemirror() {
|
||||
return this._codeEditor?.codemirror;
|
||||
}
|
||||
|
||||
get hasComments(): boolean {
|
||||
return this._codeEditor?.hasComments ?? false;
|
||||
}
|
||||
|
||||
private _onEditorSave(ev: CustomEvent): void {
|
||||
fireEvent(this, "editor-save");
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private async _copyYaml(): Promise<void> {
|
||||
if (this.yaml) {
|
||||
await copyToClipboard(this.yaml);
|
||||
|
||||
@@ -602,7 +602,7 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
// create icon
|
||||
let iconHTML = "";
|
||||
let iconHTML: string;
|
||||
if (icon) {
|
||||
const el = document.createElement("ha-icon");
|
||||
el.setAttribute("icon", icon);
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiMonitor } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import {
|
||||
areasContext,
|
||||
configContext,
|
||||
devicesContext,
|
||||
entitiesContext,
|
||||
floorsContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import { UNAVAILABLE } from "../../data/entity/entity";
|
||||
import {
|
||||
entityComboBoxKeys,
|
||||
type EntityComboBoxItem,
|
||||
} from "../../data/entity/entity_picker";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
BROWSER_PLAYER,
|
||||
type MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
} from "../../data/media-player";
|
||||
import "../entity/state-badge";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import type {
|
||||
PickerComboBoxItem,
|
||||
PickerComboBoxSearchFn,
|
||||
} from "../ha-picker-combo-box";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
interface BrowserPlayerComboBoxItem extends EntityComboBoxItem {
|
||||
id: typeof BROWSER_PLAYER;
|
||||
icon_path: string;
|
||||
stateObj?: never;
|
||||
}
|
||||
|
||||
interface MediaPlayerComboBoxItem extends EntityComboBoxItem {
|
||||
icon_path?: never;
|
||||
stateObj: MediaPlayerEntity;
|
||||
}
|
||||
|
||||
type PlayerComboBoxItem = BrowserPlayerComboBoxItem | MediaPlayerComboBoxItem;
|
||||
|
||||
@customElement("ha-media-player-picker")
|
||||
export class HaMediaPlayerPicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
private _entities!: ContextType<typeof entitiesContext>;
|
||||
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
private _devices!: ContextType<typeof devicesContext>;
|
||||
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
private _floors!: ContextType<typeof floorsContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig!: ContextType<typeof configContext>;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.value=${this.value}
|
||||
.getItems=${this._getPlayerItems}
|
||||
.rowRenderer=${this._playerRowRenderer}
|
||||
.searchFn=${this._playerSearchFn}
|
||||
.searchKeys=${entityComboBoxKeys}
|
||||
.notFoundLabel=${this._notFoundPlayerLabel}
|
||||
.popoverPlacement=${"top-end"}
|
||||
.hideClearIcon=${true}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
<slot name="field" slot="field" @click=${this.open}></slot>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
public open(ev?: Event): void {
|
||||
this._picker?.open(ev, { selectedValue: this.value });
|
||||
}
|
||||
|
||||
private _getPlayerItems = (): PlayerComboBoxItem[] => {
|
||||
const webBrowserLabel = this._i18n.localize(
|
||||
"ui.components.media-browser.web-browser"
|
||||
);
|
||||
const lang = this._i18n.language || "en";
|
||||
const isRTL =
|
||||
this._i18n.translationMetadata.translations[lang]?.isRTL || false;
|
||||
|
||||
return [
|
||||
{
|
||||
id: BROWSER_PLAYER,
|
||||
primary: webBrowserLabel,
|
||||
icon_path: mdiMonitor,
|
||||
search_labels: {
|
||||
entityName: webBrowserLabel,
|
||||
friendlyName: webBrowserLabel,
|
||||
deviceName: null,
|
||||
areaName: null,
|
||||
domainName: null,
|
||||
entityId: BROWSER_PLAYER,
|
||||
},
|
||||
},
|
||||
...Object.values(this._states)
|
||||
.filter(this._filterPlayerEntities)
|
||||
.map<MediaPlayerComboBoxItem>((stateObj) => {
|
||||
const friendlyName = computeStateName(stateObj);
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this._entities,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._floors
|
||||
);
|
||||
const entityId = stateObj.entity_id;
|
||||
const domainName = domainToName(
|
||||
this._i18n.localize,
|
||||
computeDomain(entityId)
|
||||
);
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return {
|
||||
id: entityId,
|
||||
primary,
|
||||
secondary,
|
||||
disabled: stateObj.state === UNAVAILABLE,
|
||||
domain_name: domainName,
|
||||
sorting_label: [primary, secondary].filter(Boolean).join("_"),
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
areaName: areaName || null,
|
||||
domainName: domainName || null,
|
||||
friendlyName: friendlyName || null,
|
||||
entityId,
|
||||
},
|
||||
stateObj,
|
||||
};
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
private _filterPlayerEntities = (
|
||||
entity: HassEntity
|
||||
): entity is MediaPlayerEntity =>
|
||||
computeStateDomain(entity) === "media_player" &&
|
||||
supportsFeature(entity, MediaPlayerEntityFeature.BROWSE_MEDIA) &&
|
||||
!this._entities[entity.entity_id]?.hidden;
|
||||
|
||||
private _playerRowRenderer: RenderItemFunction<PickerComboBoxItem> = (
|
||||
item: PickerComboBoxItem,
|
||||
index: number
|
||||
) => {
|
||||
const stateObj = this._isMediaPlayerItem(item) ? item.stateObj : undefined;
|
||||
|
||||
return html`
|
||||
<div
|
||||
style=${styleMap({
|
||||
width: "100%",
|
||||
borderTop: index === 0 ? undefined : "1px solid var(--divider-color)",
|
||||
})}
|
||||
>
|
||||
<ha-combo-box-item type="button" compact .disabled=${!!item.disabled}>
|
||||
${item.icon_path
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
style="margin: 0 4px"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`
|
||||
<state-badge slot="start" .stateObj=${stateObj}></state-badge>
|
||||
`}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${stateObj && this._hassConfig.userData?.showEntityIdPicker
|
||||
? html`
|
||||
<span slot="supporting-text" class="code">
|
||||
${stateObj.entity_id}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
private _playerSearchFn: PickerComboBoxSearchFn<PickerComboBoxItem> = (
|
||||
search: string,
|
||||
filteredItems: PickerComboBoxItem[]
|
||||
) => {
|
||||
const index = filteredItems.findIndex((item) => item.id === search);
|
||||
|
||||
if (index === -1) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
private _isMediaPlayerItem(
|
||||
item: PickerComboBoxItem
|
||||
): item is MediaPlayerComboBoxItem {
|
||||
return "stateObj" in item && item.stateObj !== undefined;
|
||||
}
|
||||
|
||||
private _notFoundPlayerLabel = (search: string) =>
|
||||
this._i18n.localize("ui.components.entity.entity-picker.no_match", {
|
||||
term: html`<b>${search}</b>`,
|
||||
});
|
||||
|
||||
private _valueChanged(ev: HASSDomEvent<{ value: string }>): void {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-generic-picker {
|
||||
--ha-generic-picker-width: min(360px, calc(100vw - 32px));
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-media-player-picker": HaMediaPlayerPicker;
|
||||
}
|
||||
}
|
||||
@@ -428,7 +428,8 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
this.includeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.hass.states,
|
||||
this.entityFilter
|
||||
this.entityFilter,
|
||||
!this.primaryEntitiesOnly
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
@@ -458,7 +459,8 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
this.includeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.hass.states,
|
||||
this.entityFilter
|
||||
this.entityFilter,
|
||||
!this.primaryEntitiesOnly
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
|
||||
@@ -322,6 +322,7 @@ export interface ShorthandNotCondition extends ShorthandBaseCondition {
|
||||
|
||||
export interface AutomationElementGroupCollection {
|
||||
titleKey?: LocalizeKeys;
|
||||
generic?: boolean;
|
||||
groups: AutomationElementGroup;
|
||||
}
|
||||
|
||||
|
||||
@@ -328,13 +328,12 @@ const describeLegacyTrigger = (
|
||||
let fromChoice = "other";
|
||||
let fromString = "";
|
||||
if (trigger.from !== undefined) {
|
||||
let fromArray: string[] = [];
|
||||
if (trigger.from === null) {
|
||||
if (!trigger.attribute) {
|
||||
fromChoice = "null";
|
||||
}
|
||||
} else {
|
||||
fromArray = ensureArray(trigger.from);
|
||||
const fromArray = ensureArray(trigger.from);
|
||||
|
||||
const from: string[] = [];
|
||||
for (const state of fromArray) {
|
||||
@@ -362,13 +361,12 @@ const describeLegacyTrigger = (
|
||||
let toChoice = "other";
|
||||
let toString = "";
|
||||
if (trigger.to !== undefined) {
|
||||
let toArray: string[] = [];
|
||||
if (trigger.to === null) {
|
||||
if (!trigger.attribute) {
|
||||
toChoice = "null";
|
||||
}
|
||||
} else {
|
||||
toArray = ensureArray(trigger.to);
|
||||
const toArray = ensureArray(trigger.to);
|
||||
|
||||
const to: string[] = [];
|
||||
for (const state of toArray) {
|
||||
@@ -521,7 +519,7 @@ const describeLegacyTrigger = (
|
||||
| "every_interval"
|
||||
| "on_the_xth"
|
||||
| "other"
|
||||
| "has_seconds_or_minutes" = "other";
|
||||
| "has_seconds_or_minutes";
|
||||
|
||||
let seconds = 0;
|
||||
let minutes = 0;
|
||||
|
||||
+11
-8
@@ -8,28 +8,31 @@ import type { Selector, TargetSelector } from "./selector";
|
||||
export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
{
|
||||
groups: {
|
||||
device: {},
|
||||
dynamicGroups: {},
|
||||
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
||||
time_location: {
|
||||
icon: mdiMapClock,
|
||||
members: { sun: {}, time: {}, zone: {} },
|
||||
},
|
||||
helpers: {},
|
||||
template: {},
|
||||
trigger: {},
|
||||
other: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey:
|
||||
"ui.panel.config.automation.editor.conditions.groups.helpers.label",
|
||||
"ui.panel.config.automation.editor.conditions.groups.generic.label",
|
||||
generic: true,
|
||||
groups: {
|
||||
helpers: {},
|
||||
device: {},
|
||||
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: "ui.panel.config.automation.editor.conditions.groups.other.label",
|
||||
titleKey:
|
||||
"ui.panel.config.automation.editor.conditions.groups.custom_integrations.label",
|
||||
groups: {
|
||||
template: {},
|
||||
trigger: {},
|
||||
other: {},
|
||||
customDynamicGroups: {},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
} from "../../types";
|
||||
import type { ConfigEntry } from "../config_entries";
|
||||
import type { EntityRegistryEntry } from "../entity/entity_registry";
|
||||
import type { DomainManifestLookup } from "../integration";
|
||||
import type { LabelRegistryEntry } from "../label/label_registry";
|
||||
|
||||
/**
|
||||
@@ -107,6 +108,12 @@ export const fullEntitiesContext =
|
||||
export const configEntriesContext =
|
||||
createContext<ConfigEntry[]>("configEntries");
|
||||
|
||||
/**
|
||||
* Lazy loaded integration manifests, keyed by domain.
|
||||
*/
|
||||
export const manifestsContext =
|
||||
createContext<DomainManifestLookup>("manifests");
|
||||
|
||||
// #endregion lazy-contexts
|
||||
|
||||
// #region deprecated-contexts
|
||||
|
||||
+6
-18
@@ -1235,13 +1235,7 @@ export const computeConsumptionSingle = (data: {
|
||||
(to_grid || 0) -
|
||||
(to_battery || 0);
|
||||
|
||||
let used_solar = 0;
|
||||
let grid_to_battery = 0;
|
||||
let battery_to_grid = 0;
|
||||
let solar_to_battery = 0;
|
||||
let solar_to_grid = 0;
|
||||
let used_battery = 0;
|
||||
let used_grid = 0;
|
||||
|
||||
let used_total_remaining = Math.max(used_total, 0);
|
||||
// Consumption Priority
|
||||
@@ -1266,40 +1260,34 @@ export const computeConsumptionSingle = (data: {
|
||||
|
||||
// Fill the remainder of the battery input from solar
|
||||
// Solar -> Battery_In
|
||||
solar_to_battery = Math.min(solar, to_battery);
|
||||
const solar_to_battery = Math.min(solar, to_battery);
|
||||
to_battery -= solar_to_battery;
|
||||
solar -= solar_to_battery;
|
||||
|
||||
// Solar -> Grid_Out
|
||||
solar_to_grid = Math.min(solar, to_grid);
|
||||
const solar_to_grid = Math.min(solar, to_grid);
|
||||
to_grid -= solar_to_grid;
|
||||
solar -= solar_to_grid;
|
||||
|
||||
// Battery_Out -> Grid_Out
|
||||
battery_to_grid = Math.min(from_battery, to_grid);
|
||||
const battery_to_grid = Math.min(from_battery, to_grid);
|
||||
from_battery -= battery_to_grid;
|
||||
to_grid -= battery_to_grid;
|
||||
|
||||
// Grid_In -> Battery_In (second pass)
|
||||
const grid_to_battery_2 = Math.min(from_grid, to_battery);
|
||||
grid_to_battery += grid_to_battery_2;
|
||||
from_grid -= grid_to_battery_2;
|
||||
to_battery -= grid_to_battery_2;
|
||||
|
||||
// Solar -> Consumption
|
||||
used_solar = Math.min(used_total_remaining, solar);
|
||||
const used_solar = Math.min(used_total_remaining, solar);
|
||||
used_total_remaining -= used_solar;
|
||||
solar -= used_solar;
|
||||
|
||||
// Battery_Out -> Consumption
|
||||
used_battery = Math.min(from_battery, used_total_remaining);
|
||||
from_battery -= used_battery;
|
||||
const used_battery = Math.min(from_battery, used_total_remaining);
|
||||
used_total_remaining -= used_battery;
|
||||
|
||||
// Grid_In -> Consumption
|
||||
used_grid = Math.min(used_total_remaining, from_grid);
|
||||
from_grid -= used_grid;
|
||||
used_total_remaining -= from_grid;
|
||||
const used_grid = Math.min(used_total_remaining, from_grid);
|
||||
|
||||
return {
|
||||
used_solar,
|
||||
|
||||
@@ -187,6 +187,30 @@ export const NON_NUMERIC_ATTRIBUTES = [
|
||||
"xy_color",
|
||||
];
|
||||
|
||||
export const STATE_CONDITION_HIDDEN_ATTRIBUTES = [
|
||||
"access_token",
|
||||
"available_modes",
|
||||
"color_modes",
|
||||
"editable",
|
||||
"effect_list",
|
||||
"entity_picture",
|
||||
"event_types",
|
||||
"fan_modes",
|
||||
"fan_speed_list",
|
||||
"forecast",
|
||||
"friendly_name",
|
||||
"hvac_modes",
|
||||
"icon",
|
||||
"operation_list",
|
||||
"options",
|
||||
"preset_modes",
|
||||
"sound_mode_list",
|
||||
"source_list",
|
||||
"state_class",
|
||||
"swing_modes",
|
||||
"token",
|
||||
];
|
||||
|
||||
export const computeShownAttributes = (stateObj: HassEntity) => {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
const filtersArray = STATE_ATTRIBUTES.concat(
|
||||
|
||||
@@ -53,7 +53,7 @@ export const getEntities = (
|
||||
value?: string,
|
||||
idPrefix = ""
|
||||
): EntityComboBoxItem[] => {
|
||||
let items: EntityComboBoxItem[] = [];
|
||||
let items: EntityComboBoxItem[];
|
||||
|
||||
let entityIds = Object.keys(hass.states);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { ShortcutItem } from "./home_shortcuts";
|
||||
|
||||
export interface CoreFrontendUserData {
|
||||
showAdvanced?: boolean;
|
||||
@@ -18,20 +19,12 @@ export interface CoreFrontendSystemData {
|
||||
onboarded_date?: string;
|
||||
}
|
||||
|
||||
export interface CustomShortcutItem {
|
||||
path: string;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface HomeFrontendSystemData {
|
||||
favorite_entities?: string[];
|
||||
welcome_banner_dismissed?: boolean;
|
||||
hidden_summaries?: string[];
|
||||
hide_welcome_message?: boolean;
|
||||
hide_suggested_entities?: boolean;
|
||||
custom_shortcuts?: CustomShortcutItem[];
|
||||
shortcuts?: ShortcutItem[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
export interface CustomShortcutItem {
|
||||
type: "custom";
|
||||
path: string;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface SummaryShortcutItem {
|
||||
type: "summary";
|
||||
key: string;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export type ShortcutItem = CustomShortcutItem | SummaryShortcutItem;
|
||||
|
||||
export const DEFAULT_SUMMARY_KEYS = [
|
||||
"light",
|
||||
"climate",
|
||||
"security",
|
||||
"media_players",
|
||||
"maintenance",
|
||||
"weather",
|
||||
"energy",
|
||||
] as const;
|
||||
|
||||
export type DefaultSummaryKey = (typeof DEFAULT_SUMMARY_KEYS)[number];
|
||||
|
||||
const DEFAULT_SUMMARY_KEYS_SET: ReadonlySet<string> = new Set(
|
||||
DEFAULT_SUMMARY_KEYS
|
||||
);
|
||||
|
||||
// Built-in summary keys missing from the saved list are appended in
|
||||
// DEFAULT_SUMMARY_KEYS order; summary keys no longer in the defaults are
|
||||
// dropped.
|
||||
export function resolveShortcutItems(
|
||||
saved: ShortcutItem[] | undefined
|
||||
): ShortcutItem[] {
|
||||
const result: ShortcutItem[] = [];
|
||||
const seenSummaryKeys = new Set<string>();
|
||||
for (const item of saved || []) {
|
||||
if (item.type === "summary") {
|
||||
if (!DEFAULT_SUMMARY_KEYS_SET.has(item.key)) continue;
|
||||
if (seenSummaryKeys.has(item.key)) continue;
|
||||
seenSummaryKeys.add(item.key);
|
||||
}
|
||||
result.push(item);
|
||||
}
|
||||
for (const key of DEFAULT_SUMMARY_KEYS) {
|
||||
if (!seenSummaryKeys.has(key)) {
|
||||
result.push({ type: "summary", key });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
mdiThermostat,
|
||||
mdiTimerOutline,
|
||||
mdiToggleSwitch,
|
||||
mdiVideoInputAntenna,
|
||||
mdiWater,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherPartlyCloudy,
|
||||
@@ -128,6 +129,7 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
plant: mdiFlower,
|
||||
power: mdiFlash,
|
||||
proximity: mdiAppleSafari,
|
||||
radio_frequency: mdiVideoInputAntenna,
|
||||
remote: mdiRemote,
|
||||
scene: mdiPalette,
|
||||
schedule: mdiCalendarClock,
|
||||
|
||||
@@ -109,6 +109,24 @@ export const fetchIntegrationManifests = (
|
||||
return hass.callWS<IntegrationManifest[]>(params);
|
||||
};
|
||||
|
||||
export const fetchIntegrationManifestsCollection = async (
|
||||
connection: Connection,
|
||||
setValue: (value: DomainManifestLookup) => void
|
||||
): Promise<() => void> => {
|
||||
const fetched = await connection.sendMessagePromise<IntegrationManifest[]>({
|
||||
type: "manifest/list",
|
||||
});
|
||||
const manifests: DomainManifestLookup = {};
|
||||
for (const manifest of fetched) {
|
||||
manifests[manifest.domain] = manifest;
|
||||
}
|
||||
setValue(manifests);
|
||||
// One-time fetch — nothing to unsubscribe from
|
||||
return () => {
|
||||
// noop
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchIntegrationManifest = (
|
||||
hass: HomeAssistant,
|
||||
integration: string
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
mdiPlayPause,
|
||||
mdiPodcast,
|
||||
mdiPower,
|
||||
mdiPowerOff,
|
||||
mdiPowerOn,
|
||||
mdiRepeat,
|
||||
mdiRepeatOff,
|
||||
mdiRepeatOnce,
|
||||
@@ -286,7 +288,9 @@ export const computeMediaControls = (
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!stateActive(stateObj)) {
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
|
||||
if (!stateActive(stateObj) && !assumedState) {
|
||||
return supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON)
|
||||
? [
|
||||
{
|
||||
@@ -299,14 +303,23 @@ export const computeMediaControls = (
|
||||
|
||||
const buttons: ControlButton[] = [];
|
||||
|
||||
if (
|
||||
assumedState &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON)
|
||||
) {
|
||||
buttons.push({
|
||||
icon: mdiPowerOn,
|
||||
action: "turn_on",
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_OFF)) {
|
||||
buttons.push({
|
||||
icon: mdiPower,
|
||||
icon: assumedState ? mdiPowerOff : mdiPower,
|
||||
action: "turn_off",
|
||||
});
|
||||
}
|
||||
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
const stateAttr = stateObj.attributes;
|
||||
|
||||
if (
|
||||
|
||||
@@ -12,3 +12,183 @@ export const getNumberDeviceClassConvertibleUnits = (
|
||||
type: "number/device_class_convertible_units",
|
||||
device_class: deviceClass,
|
||||
});
|
||||
|
||||
/**
|
||||
* Extracted from
|
||||
* core/homeassistant/components/sensor/const.py
|
||||
*/
|
||||
export const unitOfMeasurementOptions = [
|
||||
"%",
|
||||
"A",
|
||||
"B",
|
||||
"B/s",
|
||||
"BTU/(h⋅ft²)",
|
||||
"Beaufort",
|
||||
"CCF",
|
||||
"EB",
|
||||
"EiB",
|
||||
"GB",
|
||||
"GB/s",
|
||||
"GHz",
|
||||
"GJ",
|
||||
"GW",
|
||||
"GWh",
|
||||
"Gbit",
|
||||
"Gbit/s",
|
||||
"Gcal",
|
||||
"GiB",
|
||||
"GiB/s",
|
||||
"Hz",
|
||||
"J",
|
||||
"K",
|
||||
"KiB",
|
||||
"KiB/s",
|
||||
"L",
|
||||
"L/h",
|
||||
"L/min",
|
||||
"L/s",
|
||||
"MB",
|
||||
"MB/s",
|
||||
"MCF",
|
||||
"MHz",
|
||||
"MJ",
|
||||
"MV",
|
||||
"MW",
|
||||
"MWh",
|
||||
"Mbit",
|
||||
"Mbit/s",
|
||||
"Mcal",
|
||||
"MiB",
|
||||
"MiB/s",
|
||||
"PB",
|
||||
"Pa",
|
||||
"PiB",
|
||||
"S/cm",
|
||||
"TB",
|
||||
"TW",
|
||||
"TWh",
|
||||
"TiB",
|
||||
"V",
|
||||
"VA",
|
||||
"W",
|
||||
"W/m²",
|
||||
"Wh",
|
||||
"Wh/km",
|
||||
"YB",
|
||||
"YiB",
|
||||
"ZB",
|
||||
"ZiB",
|
||||
"ac",
|
||||
"bar",
|
||||
"bit",
|
||||
"bit/s",
|
||||
"cal",
|
||||
"cbar",
|
||||
"cm",
|
||||
"cm²",
|
||||
"d",
|
||||
"dB",
|
||||
"dBA",
|
||||
"dBm",
|
||||
"fl. oz.",
|
||||
"ft",
|
||||
"ft/s",
|
||||
"ft²",
|
||||
"ft³",
|
||||
"ft³/min",
|
||||
"g",
|
||||
"g/m³",
|
||||
"gal",
|
||||
"gal/d",
|
||||
"gal/h",
|
||||
"gal/min",
|
||||
"h",
|
||||
"hPa",
|
||||
"ha",
|
||||
"in",
|
||||
"in/d",
|
||||
"in/h",
|
||||
"in/s",
|
||||
"inHg",
|
||||
"inH₂O",
|
||||
"in²",
|
||||
"kB",
|
||||
"kB/s",
|
||||
"kHz",
|
||||
"kJ",
|
||||
"kPa",
|
||||
"kV",
|
||||
"kVA",
|
||||
"kW",
|
||||
"kWh",
|
||||
"kWh/100km",
|
||||
"kbit",
|
||||
"kbit/s",
|
||||
"kcal",
|
||||
"kg",
|
||||
"km",
|
||||
"km/h",
|
||||
"km/kWh",
|
||||
"km²",
|
||||
"kn",
|
||||
"kvar",
|
||||
"kvarh",
|
||||
"lb",
|
||||
"lx",
|
||||
"m",
|
||||
"m/min",
|
||||
"m/s",
|
||||
"mA",
|
||||
"mHz",
|
||||
"mL",
|
||||
"mL/s",
|
||||
"mPa",
|
||||
"mS/cm",
|
||||
"mV",
|
||||
"mVA",
|
||||
"mW",
|
||||
"mWh",
|
||||
"mbar",
|
||||
"mg",
|
||||
"mg/dL",
|
||||
"mg/m³",
|
||||
"mi",
|
||||
"mi/kWh",
|
||||
"min",
|
||||
"mi²",
|
||||
"mm",
|
||||
"mm/d",
|
||||
"mm/h",
|
||||
"mm/s",
|
||||
"mmHg",
|
||||
"mmol/L",
|
||||
"mm²",
|
||||
"mph",
|
||||
"ms",
|
||||
"mvar",
|
||||
"m²",
|
||||
"m³",
|
||||
"m³/h",
|
||||
"m³/min",
|
||||
"m³/s",
|
||||
"nmi",
|
||||
"oz",
|
||||
"ppb",
|
||||
"ppm",
|
||||
"psi",
|
||||
"s",
|
||||
"st",
|
||||
"var",
|
||||
"varh",
|
||||
"yd",
|
||||
"yd²",
|
||||
"°",
|
||||
"°C",
|
||||
"°F",
|
||||
"μA",
|
||||
"μS/cm",
|
||||
"μV",
|
||||
"μg",
|
||||
"μg/m³",
|
||||
"μs",
|
||||
] as const;
|
||||
|
||||
@@ -16,6 +16,7 @@ export const SCENE_IGNORED_DOMAINS = [
|
||||
"input_button",
|
||||
"persistent_notification",
|
||||
"person",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
"schedule",
|
||||
"script",
|
||||
|
||||
+24
-11
@@ -10,6 +10,7 @@ import { describeCondition } from "./automation_i18n";
|
||||
import { localizeDeviceAutomationAction } from "./device/device_automation";
|
||||
import type { EntityRegistryEntry } from "./entity/entity_registry";
|
||||
import { domainToName } from "./integration";
|
||||
import type { DomainManifestLookup } from "./integration";
|
||||
import type {
|
||||
ActionType,
|
||||
ActionTypes,
|
||||
@@ -31,12 +32,23 @@ import { getActionType } from "./script";
|
||||
const actionTranslationBaseKey =
|
||||
"ui.panel.config.automation.editor.actions.type";
|
||||
|
||||
const shouldShowDomainPrefix = (
|
||||
domain: string,
|
||||
manifests?: DomainManifestLookup
|
||||
): boolean => {
|
||||
if (!manifests) return true;
|
||||
const manifest = manifests[domain];
|
||||
if (!manifest) return true;
|
||||
return manifest.integration_type !== "entity" || !manifest.is_built_in;
|
||||
};
|
||||
|
||||
export const describeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
ignoreAlias = false,
|
||||
manifests?: DomainManifestLookup
|
||||
): string => {
|
||||
try {
|
||||
const description = tryDescribeAction(
|
||||
@@ -44,7 +56,8 @@ export const describeAction = <T extends ActionType>(
|
||||
entityRegistry,
|
||||
action,
|
||||
actionType,
|
||||
ignoreAlias
|
||||
ignoreAlias,
|
||||
manifests
|
||||
);
|
||||
if (typeof description !== "string") {
|
||||
throw new Error(String(description));
|
||||
@@ -66,7 +79,8 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
ignoreAlias = false,
|
||||
manifests?: DomainManifestLookup
|
||||
): string => {
|
||||
if (action.alias && !ignoreAlias) {
|
||||
return action.alias;
|
||||
@@ -114,20 +128,19 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
) || hass.services[domain]?.[serviceName]?.name;
|
||||
|
||||
if (config.metadata) {
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.service_name_no_targets`,
|
||||
{
|
||||
domain: domainToName(hass.localize, domain),
|
||||
name: service || config.action,
|
||||
}
|
||||
);
|
||||
if (service && shouldShowDomainPrefix(domain, manifests)) {
|
||||
return `${domainToName(hass.localize, domain)}: ${service}`;
|
||||
}
|
||||
return service || config.action;
|
||||
}
|
||||
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.service_based_on_name_no_targets`,
|
||||
{
|
||||
name: service
|
||||
? `${domainToName(hass.localize, domain)}: ${service}`
|
||||
? shouldShowDomainPrefix(domain, manifests)
|
||||
? `${domainToName(hass.localize, domain)}: ${service}`
|
||||
: service
|
||||
: config.action,
|
||||
}
|
||||
);
|
||||
|
||||
+30
-2
@@ -31,6 +31,7 @@ export type Selector =
|
||||
| AreaSelector
|
||||
| AreasDisplaySelector
|
||||
| AttributeSelector
|
||||
| AutomationBehaviorSelector
|
||||
| BooleanSelector
|
||||
| ButtonToggleSelector
|
||||
| ChooseSelector
|
||||
@@ -124,6 +125,21 @@ export interface BooleanSelector {
|
||||
boolean: {} | null;
|
||||
}
|
||||
|
||||
export type AutomationBehaviorTriggerMode = "first" | "last" | "any";
|
||||
|
||||
export type AutomationBehaviorConditionMode = "all" | "any";
|
||||
|
||||
export type AutomationBehavior =
|
||||
| AutomationBehaviorTriggerMode
|
||||
| AutomationBehaviorConditionMode;
|
||||
|
||||
export interface AutomationBehaviorSelector {
|
||||
automation_behavior: {
|
||||
mode: "trigger" | "condition";
|
||||
translation_key?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ButtonToggleSelector {
|
||||
button_toggle: {
|
||||
options: readonly string[] | readonly SelectOption[];
|
||||
@@ -249,6 +265,16 @@ interface EntitySelectorFilter {
|
||||
unit_of_measurement?: string | readonly string[];
|
||||
}
|
||||
|
||||
export interface EntitySelectorExtraOption {
|
||||
id: string;
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
icon?: string;
|
||||
icon_path?: string;
|
||||
entity_id?: string;
|
||||
hide_clear?: boolean;
|
||||
}
|
||||
|
||||
export interface EntitySelector {
|
||||
entity: {
|
||||
multiple?: boolean;
|
||||
@@ -256,6 +282,7 @@ export interface EntitySelector {
|
||||
exclude_entities?: string[];
|
||||
filter?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
reorder?: boolean;
|
||||
extra_options?: EntitySelectorExtraOption[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -453,7 +480,9 @@ export interface SelectorSelector {
|
||||
}
|
||||
|
||||
export interface SerialPortSelector {
|
||||
serial_port: {} | null;
|
||||
serial_port: {
|
||||
extra_recommended_domains?: string[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface StateSelector {
|
||||
@@ -463,7 +492,6 @@ export interface StateSelector {
|
||||
attribute?: string;
|
||||
hide_states?: string[];
|
||||
multiple?: boolean;
|
||||
no_entity?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
+27
-20
@@ -1,4 +1,5 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
@@ -58,23 +59,26 @@ export const extractFromTarget = async (
|
||||
primary_entities_only: primaryEntitiesOnly,
|
||||
});
|
||||
|
||||
export const getResolvedTargetEntityCount = async (
|
||||
hass: HomeAssistant,
|
||||
target?: HassServiceTarget
|
||||
): Promise<number | undefined> => {
|
||||
if (!target) {
|
||||
return undefined;
|
||||
export const getTargetEntityCount = (target?: HassServiceTarget): number => {
|
||||
const tempTarget = {
|
||||
entity_id: target?.entity_id ? ensureArray(target?.entity_id) : [],
|
||||
device_id: target?.device_id ? ensureArray(target?.device_id) : [],
|
||||
area_id: target?.area_id ? ensureArray(target?.area_id) : [],
|
||||
floor_id: target?.floor_id ? ensureArray(target?.floor_id) : [],
|
||||
label_id: target?.label_id ? ensureArray(target?.label_id) : [],
|
||||
};
|
||||
|
||||
if (
|
||||
tempTarget?.device_id?.length > 0 ||
|
||||
tempTarget?.area_id?.length > 0 ||
|
||||
tempTarget?.floor_id?.length > 0 ||
|
||||
tempTarget?.label_id?.length > 0
|
||||
) {
|
||||
// if targeting non entities the number of entities is dynamic
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
try {
|
||||
return (await extractFromTarget(hass, target, true)).referenced_entities
|
||||
.length;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error resolving target entity count", err);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return tempTarget?.entity_id?.length;
|
||||
};
|
||||
|
||||
export const getTriggersForTarget = async (
|
||||
@@ -118,7 +122,8 @@ export const areaMeetsFilter = (
|
||||
includeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
states?: HomeAssistant["states"],
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
includeSecondary = false
|
||||
): boolean => {
|
||||
const areaDevices = Object.values(devices).filter(
|
||||
(device) => device.area_id === area.area_id
|
||||
@@ -133,7 +138,8 @@ export const areaMeetsFilter = (
|
||||
includeDomains,
|
||||
includeDeviceClasses,
|
||||
states,
|
||||
entityFilter
|
||||
entityFilter,
|
||||
includeSecondary
|
||||
)
|
||||
)
|
||||
) {
|
||||
@@ -148,7 +154,7 @@ export const areaMeetsFilter = (
|
||||
areaEntities.some((entity) =>
|
||||
entityRegMeetsFilter(
|
||||
entity,
|
||||
false,
|
||||
includeSecondary,
|
||||
includeDomains,
|
||||
includeDeviceClasses,
|
||||
states,
|
||||
@@ -169,7 +175,8 @@ export const deviceMeetsFilter = (
|
||||
includeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
states?: HomeAssistant["states"],
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
includeSecondary = false
|
||||
): boolean => {
|
||||
const devEntities = Object.values(entities).filter(
|
||||
(entity) => entity.device_id === device.id
|
||||
@@ -179,7 +186,7 @@ export const deviceMeetsFilter = (
|
||||
!devEntities.some((entity) =>
|
||||
entityRegMeetsFilter(
|
||||
entity,
|
||||
false,
|
||||
includeSecondary,
|
||||
includeDomains,
|
||||
includeDeviceClasses,
|
||||
states,
|
||||
|
||||
+2
-1
@@ -67,6 +67,7 @@ export type ActionTraceStep =
|
||||
|
||||
interface BaseTrace {
|
||||
domain: string;
|
||||
error?: string;
|
||||
item_id: string;
|
||||
last_step: string | null;
|
||||
run_id: string;
|
||||
@@ -97,7 +98,6 @@ interface BaseTrace {
|
||||
interface BaseTraceExtended {
|
||||
trace: Record<string, ActionTraceStep[]>;
|
||||
context: Context;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface AutomationTrace extends BaseTrace {
|
||||
@@ -120,6 +120,7 @@ export interface ScriptTraceExtended extends ScriptTrace, BaseTraceExtended {
|
||||
blueprint_inputs?: BlueprintScriptConfig;
|
||||
}
|
||||
|
||||
export type Trace = AutomationTrace | ScriptTrace;
|
||||
export type TraceExtended = AutomationTraceExtended | ScriptTraceExtended;
|
||||
|
||||
interface TraceTypes {
|
||||
|
||||
+16
-13
@@ -13,9 +13,7 @@ import type { Selector, TargetSelector } from "./selector";
|
||||
export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
{
|
||||
groups: {
|
||||
device: {},
|
||||
dynamicGroups: {},
|
||||
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
||||
time_location: {
|
||||
icon: mdiMapClock,
|
||||
members: {
|
||||
@@ -26,17 +24,6 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
zone: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: "ui.panel.config.automation.editor.triggers.groups.helpers.label",
|
||||
groups: {
|
||||
helpers: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: "ui.panel.config.automation.editor.triggers.groups.other.label",
|
||||
groups: {
|
||||
event: {},
|
||||
geo_location: {},
|
||||
homeassistant: {},
|
||||
@@ -45,9 +32,25 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
template: {},
|
||||
webhook: {},
|
||||
persistent_notification: {},
|
||||
helpers: {},
|
||||
other: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: "ui.panel.config.automation.editor.triggers.groups.generic.label",
|
||||
generic: true,
|
||||
groups: {
|
||||
device: {},
|
||||
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey:
|
||||
"ui.panel.config.automation.editor.triggers.groups.custom_integrations.label",
|
||||
groups: {
|
||||
customDynamicGroups: {},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface CommonControlResult {
|
||||
export interface CommonControlsResult {
|
||||
entities: string[];
|
||||
}
|
||||
|
||||
export const getCommonControlUsagePrediction = (hass: HomeAssistant) =>
|
||||
hass.callWS<CommonControlResult>({
|
||||
export const getCommonControlsUsagePrediction = (hass: HomeAssistant) =>
|
||||
hass.callWS<CommonControlsResult>({
|
||||
type: "usage_prediction/common_control",
|
||||
});
|
||||
|
||||
@@ -5,8 +5,10 @@ export interface SerialPort {
|
||||
serial_number: string | null;
|
||||
manufacturer: string | null;
|
||||
description: string | null;
|
||||
interface_description?: string | null;
|
||||
vid?: string;
|
||||
pid?: string;
|
||||
matching_integrations: string[];
|
||||
}
|
||||
|
||||
export const scanUSBDevices = (hass: HomeAssistant) =>
|
||||
|
||||
@@ -57,6 +57,16 @@ export interface ForecastAttribute {
|
||||
wind_speed?: string;
|
||||
}
|
||||
|
||||
export type ForecastPrecipitationType = "amount" | "probability";
|
||||
|
||||
export const getForecastPrecipitation = (
|
||||
entry: ForecastAttribute,
|
||||
type: ForecastPrecipitationType
|
||||
) =>
|
||||
type === "probability"
|
||||
? entry.precipitation_probability
|
||||
: entry.precipitation;
|
||||
|
||||
interface WeatherEntityAttributes extends HassEntityAttributeBase {
|
||||
attribution?: string;
|
||||
humidity?: number;
|
||||
|
||||
@@ -241,10 +241,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.integrations.config_flow.${
|
||||
devicesLength ? "device_created" : "success"
|
||||
}`,
|
||||
{
|
||||
number: devicesLength,
|
||||
}
|
||||
}`
|
||||
);
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -104,6 +104,7 @@ class StepFlowForm extends LitElement {
|
||||
.computeHelper=${this._helperCallback}
|
||||
.computeError=${this._errorCallback}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
.context=${{ handler: step.handler }}
|
||||
></ha-form>
|
||||
</div>
|
||||
${step.preview
|
||||
|
||||
@@ -14,6 +14,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
@@ -28,14 +29,15 @@ import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import type { HaIconButton } from "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-marquee-text";
|
||||
import "../../../components/ha-select";
|
||||
import type { HaSlider } from "../../../components/ha-slider";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { showJoinMediaPlayersDialog } from "../../../components/media-player/show-join-media-players-dialog";
|
||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import type {
|
||||
MediaPickedEvent,
|
||||
@@ -205,27 +207,31 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-dropdown @wa-select=${this._handleSourceChange}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(`ui.card.media_player.source`)}
|
||||
.path=${mdiLoginVariant}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${this.stateObj.attributes.source_list!.map(
|
||||
(source) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${source}
|
||||
.selected=${source === this.stateObj?.attributes.source}
|
||||
>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"source",
|
||||
source
|
||||
)}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
</ha-dropdown>`;
|
||||
return html`<ha-tooltip for="source-button">
|
||||
${this.hass.localize(`ui.card.media_player.source`)}
|
||||
</ha-tooltip>
|
||||
<ha-dropdown @wa-select=${this._handleSourceChange}>
|
||||
<ha-icon-button
|
||||
id="source-button"
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(`ui.card.media_player.source`)}
|
||||
.path=${mdiLoginVariant}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${this.stateObj.attributes.source_list!.map(
|
||||
(source) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${source}
|
||||
.selected=${source === this.stateObj?.attributes.source}
|
||||
>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"source",
|
||||
source
|
||||
)}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
</ha-dropdown>`;
|
||||
}
|
||||
|
||||
protected _renderSoundMode() {
|
||||
@@ -240,27 +246,31 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-dropdown @wa-select=${this._handleSoundModeChange}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(`ui.card.media_player.sound_mode`)}
|
||||
.path=${mdiMusicNoteEighth}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${this.stateObj.attributes.sound_mode_list!.map(
|
||||
(soundMode) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${soundMode}
|
||||
.selected=${soundMode === this.stateObj?.attributes.sound_mode}
|
||||
>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"sound_mode",
|
||||
soundMode
|
||||
)}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
</ha-dropdown>`;
|
||||
return html`<ha-tooltip for="sound-mode-button">
|
||||
${this.hass.localize(`ui.card.media_player.sound_mode`)}
|
||||
</ha-tooltip>
|
||||
<ha-dropdown @wa-select=${this._handleSoundModeChange}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
id="sound-mode-button"
|
||||
.path=${mdiMusicNoteEighth}
|
||||
hide-title
|
||||
>
|
||||
</ha-icon-button>
|
||||
${this.stateObj.attributes.sound_mode_list!.map(
|
||||
(soundMode) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${soundMode}
|
||||
.selected=${soundMode === this.stateObj?.attributes.sound_mode}
|
||||
>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"sound_mode",
|
||||
soundMode
|
||||
)}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
</ha-dropdown>`;
|
||||
}
|
||||
|
||||
protected _renderGrouping() {
|
||||
@@ -275,16 +285,20 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
const hasMultipleMembers = groupMembers && groupMembers?.length > 1;
|
||||
|
||||
return html`<ha-icon-button
|
||||
@click=${this._showGroupMediaPlayers}
|
||||
.title=${this.hass.localize("ui.card.media_player.join")}
|
||||
>
|
||||
<div class="grouping">
|
||||
<ha-svg-icon .path=${mdiSpeakerMultiple}></ha-svg-icon>
|
||||
${hasMultipleMembers
|
||||
? html`<span class="badge">${groupMembers?.length || 4}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-icon-button>`;
|
||||
@click=${this._showGroupMediaPlayers}
|
||||
.title=${this.hass.localize("ui.card.media_player.join")}
|
||||
id="grouping-button"
|
||||
>
|
||||
<div class="grouping">
|
||||
<ha-svg-icon .path=${mdiSpeakerMultiple}></ha-svg-icon>
|
||||
${hasMultipleMembers
|
||||
? html`<span class="badge">${groupMembers?.length || 4}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-icon-button>
|
||||
<ha-tooltip for="grouping-button">
|
||||
${this.hass.localize("ui.card.media_player.join")}
|
||||
</ha-tooltip>`;
|
||||
}
|
||||
|
||||
protected _renderEmptyCover(title: string, icon?: string) {
|
||||
@@ -450,46 +464,61 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
<div class="controls-row">
|
||||
${!isUnavailableState(stateObj.state) &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
@click=${this._showBrowseMedia}
|
||||
.title=${this.hass.localize(
|
||||
"ui.card.media_player.browse_media"
|
||||
)}
|
||||
.path=${mdiPlayBoxMultiple}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
? this._renderControlButton(
|
||||
"browse_media",
|
||||
this.hass.localize("ui.card.media_player.browse_media"),
|
||||
mdiPlayBoxMultiple,
|
||||
this._showBrowseMedia
|
||||
)
|
||||
: nothing}
|
||||
${this._renderGrouping()} ${this._renderSourceControl()}
|
||||
${this._renderSoundMode()}
|
||||
${turnOn
|
||||
? html`<ha-icon-button
|
||||
action=${turnOn.action}
|
||||
@click=${this._handleClick}
|
||||
.title=${this.hass.localize(
|
||||
`ui.card.media_player.${turnOn.action}`
|
||||
)}
|
||||
.path=${turnOn.icon}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
? this._renderControlButton(
|
||||
"turn_on",
|
||||
this.hass.localize(`ui.card.media_player.${turnOn.action}`),
|
||||
turnOn.icon,
|
||||
this._handleClick,
|
||||
turnOn.action
|
||||
)
|
||||
: nothing}
|
||||
${turnOff
|
||||
? html`<ha-icon-button
|
||||
action=${turnOff.action}
|
||||
@click=${this._handleClick}
|
||||
.title=${this.hass.localize(
|
||||
`ui.card.media_player.${turnOff.action}`
|
||||
)}
|
||||
.path=${turnOff.icon}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
? this._renderControlButton(
|
||||
"turn_off",
|
||||
this.hass.localize(`ui.card.media_player.${turnOff.action}`),
|
||||
turnOff.icon,
|
||||
this._handleClick,
|
||||
turnOff.action
|
||||
)
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderControlButton(
|
||||
idSuffix: string,
|
||||
title: string,
|
||||
icon: string,
|
||||
clickHandler: (e: MouseEvent) => void,
|
||||
action?: string
|
||||
) {
|
||||
return html`
|
||||
<ha-icon-button
|
||||
.id=${`media-control-row-button-${idSuffix}`}
|
||||
hide-title
|
||||
.action=${action}
|
||||
@click=${clickHandler}
|
||||
.label=${title}
|
||||
.path=${icon}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<ha-tooltip for=${`media-control-row-button-${idSuffix}`}>
|
||||
${title}
|
||||
</ha-tooltip>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
@@ -655,6 +684,8 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
|
||||
.controls-row ha-icon-button {
|
||||
color: var(--secondary-text-color);
|
||||
background: var(--ha-color-fill-neutral-quiet-resting);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
}
|
||||
|
||||
.controls-row ha-svg-icon {
|
||||
@@ -678,7 +709,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
handleMediaControlClick(
|
||||
this.hass!,
|
||||
this.stateObj!,
|
||||
(e.currentTarget as HTMLElement).getAttribute("action")!
|
||||
(e.currentTarget as HaIconButton & { action: string }).action!
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -495,8 +495,12 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _goToAddEntityTo(ev) {
|
||||
// Only check for request-selected events (from menu items), not regular clicks (from icon button)
|
||||
if (ev.type === "request-selected" && !shouldHandleRequestSelectedEvent(ev))
|
||||
if (
|
||||
ev.type === "request-selected" &&
|
||||
!shouldHandleRequestSelectedEvent(ev)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._setView("add_to");
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,11 @@ class MoreInfoContent extends LitElement {
|
||||
|
||||
if (!moreInfoType) return nothing;
|
||||
|
||||
const memberIds = this._getEntityMemberIds(this.stateObj);
|
||||
const memberIds = this._getEntityMemberIds(
|
||||
this.stateObj,
|
||||
this.entry,
|
||||
this.hass.entities
|
||||
);
|
||||
|
||||
return html`
|
||||
${dynamicElement(moreInfoType, {
|
||||
@@ -75,23 +79,27 @@ class MoreInfoContent extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _getEntityMemberIds(stateObj: HassEntity): string[] | undefined {
|
||||
if (computeStateDomain(stateObj) === "group") {
|
||||
// Don't show entity members for legacy groups as they already show
|
||||
// the members in their more info dialog.
|
||||
return undefined;
|
||||
private _getEntityMemberIds = memoizeOne(
|
||||
(
|
||||
stateObj: HassEntity,
|
||||
entry: ExtEntityRegistryEntry | null | undefined,
|
||||
entities: HomeAssistant["entities"]
|
||||
): string[] | undefined => {
|
||||
if (computeStateDomain(stateObj) === "group") {
|
||||
// Don't show entity members for legacy groups as they already show
|
||||
// the members in their more info dialog.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const memberIds =
|
||||
(entry?.capabilities?.group_entities as string[] | undefined) ??
|
||||
(Array.isArray(stateObj.attributes.entity_id)
|
||||
? (stateObj.attributes.entity_id as string[])
|
||||
: undefined);
|
||||
|
||||
return memberIds?.filter((entityId) => !entities[entityId]?.hidden);
|
||||
}
|
||||
|
||||
const memberIds =
|
||||
(this.entry?.capabilities?.group_entities as string[] | undefined) ??
|
||||
(Array.isArray(stateObj.attributes.entity_id)
|
||||
? (stateObj.attributes.entity_id as string[])
|
||||
: undefined);
|
||||
|
||||
return memberIds?.filter(
|
||||
(entityId) => !this.hass!.entities[entityId]?.hidden
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _entitiesSectionConfig = memoizeOne((entityIds: string[]) => {
|
||||
const hass = this.hass!;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-adaptive-dialog";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-expansion-panel";
|
||||
import "../../components/ha-fade-in";
|
||||
import "../../components/animation/ha-fade-in";
|
||||
import "../../components/ha-icon-next";
|
||||
import "../../components/ha-md-list";
|
||||
import "../../components/ha-md-list-item";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user