mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-16 15:18:16 +00:00
Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ecb41accd | ||
|
|
68cbaf6481 | ||
|
|
b9b249a317 | ||
|
|
df6578dfdf | ||
|
|
b4a07f504c | ||
|
|
24b8078a9c | ||
|
|
2bd8a657b8 | ||
|
|
61724a52ba | ||
|
|
67d73261a4 | ||
|
|
419ba64bed | ||
|
|
e87155bba3 | ||
|
|
d8a3939e22 | ||
|
|
5141b6aea9 | ||
|
|
9aaf1bdd07 | ||
|
|
a0e7c8e9bb | ||
|
|
8f482776b9 | ||
|
|
2c80183ded | ||
|
|
9bd5e25ee3 | ||
|
|
994193465f | ||
|
|
530b7ed457 | ||
|
|
24332e8288 | ||
|
|
980aaa2bac | ||
|
|
344856cbc4 | ||
|
|
06877cbaaa | ||
|
|
e1ac7e98b0 | ||
|
|
4b2426ab77 | ||
|
|
f9471d6b4c | ||
|
|
0645484258 | ||
|
|
2da3efb812 | ||
|
|
e3674e550f | ||
|
|
cff038fea4 | ||
|
|
bed39fd8e9 | ||
|
|
b8346d0286 | ||
|
|
5f80b74322 | ||
|
|
73e0f1e18d | ||
|
|
0b16b735e8 | ||
|
|
c88dcf10b9 | ||
|
|
08bc23e2e7 | ||
|
|
97d49f06b1 | ||
|
|
50b727393d | ||
|
|
5c6dd2a697 |
@@ -5,6 +5,9 @@ updates:
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "06:00"
|
||||
cooldown:
|
||||
default-days-before-reopen: 30
|
||||
default-days: 7
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- Dependencies
|
||||
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -24,6 +27,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
@@ -59,6 +63,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -18,6 +18,9 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint and check format
|
||||
@@ -25,6 +28,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
@@ -59,6 +64,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
@@ -77,6 +84,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [dev]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
@@ -28,6 +32,7 @@ jobs:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
|
||||
@@ -9,6 +9,9 @@ on:
|
||||
- dev
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -25,6 +28,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
@@ -60,6 +64,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -5,6 +5,9 @@ on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -17,6 +20,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -10,6 +10,9 @@ on:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -22,6 +25,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: "Pull Request Labeler"
|
||||
|
||||
on: pull_request_target
|
||||
on: pull_request_target # zizmor: ignore[dangerous-triggers] -- safe: only runs actions/labeler, no PR code checkout
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -21,6 +21,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
name: RelativeCI
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] -- safe: only downloads artifacts, no PR code checkout
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
name: Upload stats
|
||||
upload-frontend-modern:
|
||||
name: Upload stats (frontend/modern)
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
strategy:
|
||||
matrix:
|
||||
bundle: [frontend]
|
||||
build: [modern, legacy]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
key: ${{ secrets.RELATIVE_CI_KEY_frontend_modern }}
|
||||
token: ${{ github.token }}
|
||||
artifactName: ${{ format('{0}-bundle-stats', matrix.bundle) }}
|
||||
webpackStatsFile: ${{ format('{0}-{1}.json', matrix.bundle, matrix.build) }}
|
||||
artifactName: frontend-bundle-stats
|
||||
webpackStatsFile: frontend-modern.json
|
||||
|
||||
upload-frontend-legacy:
|
||||
name: Upload stats (frontend/legacy)
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
with:
|
||||
key: ${{ secrets.RELATIVE_CI_KEY_frontend_legacy }}
|
||||
token: ${{ github.token }}
|
||||
artifactName: frontend-bundle-stats
|
||||
webpackStatsFile: frontend-legacy.json
|
||||
|
||||
@@ -27,6 +27,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
@@ -34,13 +36,12 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
uses: home-assistant/actions/helpers/verify-version@d56d093b9ab8d2105bc0cb6ee9bcc0ef4ec8b96d # master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
@@ -62,11 +63,10 @@ jobs:
|
||||
skip-existing: true
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: gh release upload "$TAG_NAME" dist/*.whl dist/*.tar.gz --clobber
|
||||
|
||||
wheels-init:
|
||||
name: Init wheels build
|
||||
@@ -74,15 +74,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate requirements.txt
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: |
|
||||
# Sleep to give pypi time to populate the new version across mirrors
|
||||
sleep 240
|
||||
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
|
||||
version=$(echo "$GITHUB_REF" | awk -F"/" '{print $NF}' )
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
# home-assistant/wheels doesn't support SHA pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/[email protected]
|
||||
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
with:
|
||||
abi: cp314
|
||||
tag: musllinux_1_2
|
||||
@@ -99,11 +101,12 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Download Translations
|
||||
@@ -113,8 +116,11 @@ jobs:
|
||||
- name: Build landing-page
|
||||
run: landing-page/script/build_landing_page
|
||||
- name: Tar folder
|
||||
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
||||
env:
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: tar -czf "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" -C landing-page/dist .
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: gh release upload "$TAG_NAME" "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" --clobber
|
||||
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
paths:
|
||||
- src/translations/en.json
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
name: Upload
|
||||
@@ -15,6 +18,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
@@ -19,7 +19,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
|
||||
We want to make it as easy for designers to contribute as it is for developers. There’s a lot a designer can contribute to:
|
||||
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Start designing with our <a href="https://www.figma.com/design/2WGI8IDGyxINjSV6NRvPur/Home-Assistant-Design-Kit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
## Developers
|
||||
|
||||
+6
-8
@@ -30,7 +30,7 @@
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.1",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
"@codemirror/language": "6.12.2",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
@@ -129,7 +129,6 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.9",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.4.0",
|
||||
"workbox-core": "7.4.0",
|
||||
@@ -151,7 +150,7 @@
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.5",
|
||||
"@rspack/core": "1.7.9",
|
||||
"@rspack/core": "1.7.10",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -169,9 +168,8 @@
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.1.0",
|
||||
"@vitest/coverage-v8": "4.1.1",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
@@ -206,13 +204,13 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "21.0.3",
|
||||
"tar": "7.5.12",
|
||||
"tar": "7.5.13",
|
||||
"terser-webpack-plugin": "5.4.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.57.1",
|
||||
"typescript-eslint": "8.57.2",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.0",
|
||||
"vitest": "4.1.1",
|
||||
"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"
|
||||
|
||||
@@ -4,11 +4,14 @@ import type {
|
||||
EntityRegistryEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { stripPrefixFromEntityName } from "./strip_prefix_from_entity_name";
|
||||
|
||||
export const computeEntityName = (
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"]
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): string | undefined => {
|
||||
const entry = entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
@@ -18,22 +21,49 @@ export const computeEntityName = (
|
||||
// Fall back to state name if not in the entity registry (friendly name)
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
return computeEntityEntryName(entry);
|
||||
return computeEntityEntryName(entry, devices);
|
||||
};
|
||||
|
||||
export const computeEntityEntryName = (
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry,
|
||||
devices: HomeAssistant["devices"],
|
||||
fallbackStateObj?: HassEntity
|
||||
): string | undefined => {
|
||||
if (entry.name != null) {
|
||||
return entry.name;
|
||||
const name =
|
||||
entry.name ||
|
||||
("original_name" in entry && entry.original_name != null
|
||||
? String(entry.original_name)
|
||||
: undefined);
|
||||
|
||||
const device = entry.device_id ? devices[entry.device_id] : undefined;
|
||||
|
||||
if (!device) {
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
if (fallbackStateObj) {
|
||||
return computeStateName(fallbackStateObj);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if ("original_name" in entry && entry.original_name != null) {
|
||||
return String(entry.original_name);
|
||||
|
||||
const deviceName = computeDeviceName(device);
|
||||
|
||||
// If the device name is the same as the entity name, consider empty entity name
|
||||
if (deviceName === name) {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
// Remove the device name from the entity name if it starts with it
|
||||
if (deviceName && name) {
|
||||
return stripPrefixFromEntityName(name, deviceName) || name;
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
export const entityUseDeviceName = (
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"]
|
||||
): boolean => !computeEntityName(stateObj, entities);
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): boolean => !computeEntityName(stateObj, entities, devices);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { computeAreaName } from "./compute_area_name";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeEntityName, entityUseDeviceName } from "./compute_entity_name";
|
||||
import { computeFloorName } from "./compute_floor_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { getEntityContext } from "./context/get_entity_context";
|
||||
|
||||
const DEFAULT_SEPARATOR = " ";
|
||||
@@ -29,14 +30,23 @@ export interface EntityNameOptions {
|
||||
|
||||
export const computeEntityNameDisplay = (
|
||||
stateObj: HassEntity,
|
||||
name: EntityNameItem | EntityNameItem[] | undefined,
|
||||
name: string | EntityNameItem | EntityNameItem[] | undefined,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"],
|
||||
options?: EntityNameOptions
|
||||
) => {
|
||||
let items = ensureArray(name || DEFAULT_ENTITY_NAME);
|
||||
if (typeof name === "string") {
|
||||
return name;
|
||||
}
|
||||
|
||||
// If no name config is provided, fall back to the friendly name
|
||||
if (!name) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
|
||||
let items = ensureArray(name);
|
||||
|
||||
const separator = options?.separator ?? DEFAULT_SEPARATOR;
|
||||
|
||||
@@ -45,7 +55,7 @@ export const computeEntityNameDisplay = (
|
||||
return items.map((item) => item.text).join(separator);
|
||||
}
|
||||
|
||||
const useDeviceName = entityUseDeviceName(stateObj, entities);
|
||||
const useDeviceName = entityUseDeviceName(stateObj, entities, devices);
|
||||
|
||||
// If entity uses device name, and device is not already included, replace it with device name
|
||||
if (useDeviceName) {
|
||||
@@ -91,7 +101,7 @@ export const computeEntityNameList = (
|
||||
const names = name.map((item) => {
|
||||
switch (item.type) {
|
||||
case "entity":
|
||||
return computeEntityName(stateObj, entities);
|
||||
return computeEntityName(stateObj, entities, devices);
|
||||
case "device":
|
||||
return device ? computeDeviceName(device) : undefined;
|
||||
case "area":
|
||||
|
||||
@@ -142,9 +142,10 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
minusSign: "value",
|
||||
plusSign: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
minusSign: "value",
|
||||
};
|
||||
|
||||
const valueParts: ValuePart[] = [];
|
||||
@@ -153,7 +154,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
|
||||
// Merge consecutive value parts (e.g. "-" + "12" + "." + "00" → "-12.00")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Indicates whether the current browser supports the Popover API.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Popover_API
|
||||
*/
|
||||
export const popoverSupported = globalThis?.HTMLElement?.prototype
|
||||
? Object.prototype.hasOwnProperty.call(
|
||||
globalThis.HTMLElement.prototype,
|
||||
"popover"
|
||||
)
|
||||
: false;
|
||||
@@ -71,13 +71,6 @@ export const formatNumberToParts = (
|
||||
? numberFormatToLocale(localeOptions)
|
||||
: undefined;
|
||||
|
||||
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
|
||||
Number.isNaN =
|
||||
Number.isNaN ||
|
||||
function isNaN(input) {
|
||||
return typeof input === "number" && isNaN(input);
|
||||
};
|
||||
|
||||
if (
|
||||
localeOptions?.number_format !== NumberFormat.none &&
|
||||
!Number.isNaN(Number(num))
|
||||
|
||||
@@ -28,6 +28,13 @@ const safeParseFloat = (value) => {
|
||||
return isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const CLIMATE_MODE_CONFIGS = [
|
||||
{ mode: "heat", action: "heating", cssVar: "--state-climate-heat-color" },
|
||||
{ mode: "cool", action: "cooling", cssVar: "--state-climate-cool-color" },
|
||||
{ mode: "dry", action: "drying", cssVar: "--state-climate-dry-color" },
|
||||
{ mode: "fan_only", action: "fan", cssVar: "--state-climate-fan_only-color" },
|
||||
] as const;
|
||||
|
||||
@customElement("state-history-chart-line")
|
||||
export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -429,23 +436,18 @@ export class StateHistoryChartLine extends LitElement {
|
||||
(entityState) => entityState.attributes?.hvac_action
|
||||
);
|
||||
|
||||
const isHeating =
|
||||
domain === "climate" && hasHvacAction
|
||||
? (entityState: LineChartState) =>
|
||||
CLIMATE_HVAC_ACTION_TO_MODE[
|
||||
entityState.attributes?.hvac_action
|
||||
] === "heat"
|
||||
: (entityState: LineChartState) => entityState.state === "heat";
|
||||
const isCooling =
|
||||
domain === "climate" && hasHvacAction
|
||||
? (entityState: LineChartState) =>
|
||||
CLIMATE_HVAC_ACTION_TO_MODE[
|
||||
entityState.attributes?.hvac_action
|
||||
] === "cool"
|
||||
: (entityState: LineChartState) => entityState.state === "cool";
|
||||
|
||||
const hasHeat = states.states.some(isHeating);
|
||||
const hasCool = states.states.some(isCooling);
|
||||
const activeModes = CLIMATE_MODE_CONFIGS.map(
|
||||
({ mode, action, cssVar }) => {
|
||||
const isActive =
|
||||
domain === "climate" && hasHvacAction
|
||||
? (entityState: LineChartState) =>
|
||||
CLIMATE_HVAC_ACTION_TO_MODE[
|
||||
entityState.attributes?.hvac_action
|
||||
] === mode
|
||||
: (entityState: LineChartState) => entityState.state === mode;
|
||||
return { action, cssVar, isActive };
|
||||
}
|
||||
).filter(({ isActive }) => states.states.some(isActive));
|
||||
// We differentiate between thermostats that have a target temperature
|
||||
// range versus ones that have just a target temperature
|
||||
|
||||
@@ -466,33 +468,19 @@ export class StateHistoryChartLine extends LitElement {
|
||||
"component.climate.entity_component._.state_attributes.current_temperature.name"
|
||||
)
|
||||
);
|
||||
if (hasHeat) {
|
||||
for (const { action, cssVar } of activeModes) {
|
||||
addDataSet(
|
||||
states.entity_id + "-heating",
|
||||
`${states.entity_id}-${action}`,
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.heating", { name: name })
|
||||
? this.hass.localize(`ui.card.climate.${action}`, {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.heating"
|
||||
`component.climate.entity_component._.state_attributes.hvac_action.state.${action}`
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-climate-heat-color"),
|
||||
computedStyles.getPropertyValue(cssVar),
|
||||
true
|
||||
);
|
||||
// The "heating" series uses steppedArea to shade the area below the current
|
||||
// temperature when the thermostat is calling for heat.
|
||||
}
|
||||
if (hasCool) {
|
||||
addDataSet(
|
||||
states.entity_id + "-cooling",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.cooling", { name: name })
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.cooling"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-climate-cool-color"),
|
||||
true
|
||||
);
|
||||
// The "cooling" series uses steppedArea to shade the area below the current
|
||||
// temperature when the thermostat is calling for heat.
|
||||
}
|
||||
|
||||
if (hasTargetRange) {
|
||||
@@ -540,11 +528,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
entityState.attributes.current_temperature
|
||||
);
|
||||
const series = [curTemp];
|
||||
if (hasHeat) {
|
||||
series.push(isHeating(entityState) ? curTemp : null);
|
||||
}
|
||||
if (hasCool) {
|
||||
series.push(isCooling(entityState) ? curTemp : null);
|
||||
for (const { isActive } of activeModes) {
|
||||
series.push(isActive(entityState) ? curTemp : null);
|
||||
}
|
||||
if (hasTargetRange) {
|
||||
const targetHigh = safeParseFloat(
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
DEFAULT_ENTITY_NAME,
|
||||
type EntityNameItem,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import type { EntityNameType } from "../../common/translations/entity-state";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
@@ -17,12 +15,14 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-button-toggle-group";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import "../ha-sortable";
|
||||
import "../input/ha-input";
|
||||
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
@@ -73,10 +73,291 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||
@state() private _mode?: "composed" | "custom";
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
const items = this._toItems(this.value);
|
||||
this._mode =
|
||||
items.length === 1 && items[0].type === "text" ? "custom" : "composed";
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (this._mode === undefined) {
|
||||
const items = this._toItems(this.value);
|
||||
this._mode =
|
||||
items.length === 1 && items[0].type === "text" ? "custom" : "composed";
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const modeButtons = [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.mode_composed"
|
||||
),
|
||||
value: "composed",
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.mode_custom"
|
||||
),
|
||||
value: "custom",
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
.buttons=${modeButtons}
|
||||
.active=${this._mode}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._modeChanged}
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._mode === "custom"
|
||||
? this._renderTextInput()
|
||||
: this._renderPicker()}
|
||||
</div>
|
||||
</div>
|
||||
${this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTextInput() {
|
||||
const items = this._items;
|
||||
const value =
|
||||
items.length === 1 && items[0].type === "text" ? items[0].text || "" : "";
|
||||
return html`
|
||||
<ha-input
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.value=${value}
|
||||
@input=${this._textInputChanged}
|
||||
></ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPicker() {
|
||||
const value = this._items;
|
||||
const validTypes = this._validTypes(this.entityId);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.value=${this._getPickerValue()}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
.searchFn=${this._searchFn}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.search"
|
||||
)}
|
||||
>
|
||||
<div slot="field" class="field">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item,
|
||||
(item: EntityNameItem, idx) => {
|
||||
const label = this._formatItem(item);
|
||||
const isValid = validTypes.has(item.type);
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _modeChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._mode = ev.detail.value as "composed" | "custom";
|
||||
}
|
||||
|
||||
private _textInputChanged(ev: Event) {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
const newValue: EntityNameItem[] = value
|
||||
? [{ type: "text", text: value }]
|
||||
: [];
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _addItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._editIndex = undefined;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private async _editItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
||||
10
|
||||
);
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
const value = this._items[idx];
|
||||
// Pre-fill the field value when editing a text item
|
||||
if (value.type === "text" && value.text) {
|
||||
this._picker?.setFieldValue(value.text);
|
||||
}
|
||||
}
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const value = this._items;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const value = [...this._items];
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item: EntityNameItem = parseOptionValue(value);
|
||||
|
||||
const newValue = [...this._items];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = item;
|
||||
this._editIndex = undefined;
|
||||
} else {
|
||||
newValue.push(item);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
|
||||
if (this._picker) {
|
||||
this._picker.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: EntityNameItem[]) {
|
||||
const newValue = this._toValue(value);
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private get _items(): EntityNameItem[] {
|
||||
return this._toItems(this.value);
|
||||
}
|
||||
|
||||
private _toItems = memoizeOne((value?: typeof this.value) => {
|
||||
if (typeof value === "string") {
|
||||
if (value === "") {
|
||||
return [];
|
||||
}
|
||||
return [{ type: "text", text: value } satisfies EntityNameItem];
|
||||
}
|
||||
return value ? ensureArray(value) : [];
|
||||
});
|
||||
|
||||
private _toValue = memoizeOne(
|
||||
(items: EntityNameItem[]): typeof this.value => {
|
||||
if (items.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
return item.type === "text" ? item.text : item;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _formatItem = (item: EntityNameItem) => {
|
||||
if (item.type === "text") {
|
||||
return `"${item.text}"`;
|
||||
}
|
||||
if (KNOWN_TYPES.has(item.type)) {
|
||||
return this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
|
||||
);
|
||||
}
|
||||
return item.type;
|
||||
};
|
||||
|
||||
private _validTypes = memoizeOne((entityId?: string) => {
|
||||
const options = new Set<string>(["text"]);
|
||||
if (!entityId) {
|
||||
@@ -161,157 +442,6 @@ export class HaEntityNamePicker extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _formatItem = (item: EntityNameItem) => {
|
||||
if (item.type === "text") {
|
||||
return `"${item.text}"`;
|
||||
}
|
||||
if (KNOWN_TYPES.has(item.type)) {
|
||||
return this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
|
||||
);
|
||||
}
|
||||
return item.type;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
const value = this._items;
|
||||
const validTypes = this._validTypes(this.entityId);
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.value=${this._getPickerValue()}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
.searchFn=${this._searchFn}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.search"
|
||||
)}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item,
|
||||
(item: EntityNameItem, idx) => {
|
||||
const label = this._formatItem(item);
|
||||
const isValid = validTypes.has(item.type);
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
</ha-generic-picker>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private async _addItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._editIndex = undefined;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private async _editItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
||||
10
|
||||
);
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
const value = this._items[idx];
|
||||
// Pre-fill the field value when editing a text item
|
||||
if (value.type === "text" && value.text) {
|
||||
this._picker?.setFieldValue(value.text);
|
||||
}
|
||||
}
|
||||
|
||||
private get _items(): EntityNameItem[] {
|
||||
return this._toItems(this.value);
|
||||
}
|
||||
|
||||
private _toItems = memoizeOne((value?: typeof this.value) => {
|
||||
if (typeof value === "string") {
|
||||
if (value === "") {
|
||||
return [];
|
||||
}
|
||||
return [{ type: "text", text: value } satisfies EntityNameItem];
|
||||
}
|
||||
return value ? ensureArray(value) : [...DEFAULT_ENTITY_NAME];
|
||||
});
|
||||
|
||||
private _toValue = memoizeOne(
|
||||
(items: EntityNameItem[]): typeof this.value => {
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
}
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
return item.type === "text" ? item.text : item;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getPickerValue(): string | undefined {
|
||||
if (this._editIndex != null) {
|
||||
const item = this._items[this._editIndex];
|
||||
@@ -362,58 +492,6 @@ export class HaEntityNamePicker extends LitElement {
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const value = this._items;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const value = [...this._items];
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item: EntityNameItem = parseOptionValue(value);
|
||||
|
||||
const newValue = [...this._items];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = item;
|
||||
this._editIndex = undefined;
|
||||
} else {
|
||||
newValue.push(item);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
|
||||
if (this._picker) {
|
||||
this._picker.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: EntityNameItem[]) {
|
||||
const newValue = this._toValue(value);
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
@@ -421,13 +499,42 @@ export class HaEntityNamePicker extends LitElement {
|
||||
}
|
||||
|
||||
.container {
|
||||
--ha-input-padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
ha-generic-picker,
|
||||
ha-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field {
|
||||
position: relative;
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-end-end-radius: var(--ha-border-radius-square);
|
||||
border-end-start-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
.container:after {
|
||||
.field:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -445,30 +552,25 @@ export class HaEntityNamePicker extends LitElement {
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
:host([disabled]) .container:after {
|
||||
:host([disabled]) .field:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
.container:focus-within:after {
|
||||
.field:focus-within:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
|
||||
.add {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-2) var(--ha-space-2);
|
||||
}
|
||||
|
||||
.invalid {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-relative-time";
|
||||
import "./state-badge";
|
||||
import "../ha-tooltip";
|
||||
import "./state-badge";
|
||||
|
||||
@customElement("state-info")
|
||||
class StateInfo extends LitElement {
|
||||
@@ -22,7 +21,7 @@ class StateInfo extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const name = computeStateName(this.stateObj);
|
||||
const name = this.hass.formatEntityName(this.stateObj, { type: "entity" });
|
||||
|
||||
return html`<state-badge
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -56,7 +56,10 @@ class HaAttributeValue extends LitElement {
|
||||
this.stateObj!,
|
||||
this.attribute
|
||||
);
|
||||
return parts.find((part) => part.type === "value")?.value;
|
||||
return parts
|
||||
.filter((part) => part.type === "value")
|
||||
.map((part) => part.value)
|
||||
.join("");
|
||||
}
|
||||
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
|
||||
|
||||
@@ -57,6 +57,7 @@ export class HaButton extends Button {
|
||||
.button {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: 1;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
transition: background-color var(--ha-animation-duration-fast)
|
||||
ease-out;
|
||||
|
||||
@@ -142,6 +142,19 @@ export const computeInitialHaFormData = (
|
||||
])[firstChoice],
|
||||
};
|
||||
}
|
||||
} else if ("numeric_threshold" in selector) {
|
||||
const mode = selector.numeric_threshold?.mode ?? "crossed";
|
||||
const type = mode === "changed" ? "any" : "above";
|
||||
data[field.name] =
|
||||
type === "any"
|
||||
? { type }
|
||||
: {
|
||||
type,
|
||||
value: {
|
||||
number: selector.numeric_threshold?.number?.min ?? 0,
|
||||
active_choice: "number",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
|
||||
@@ -121,6 +121,8 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
css`
|
||||
ha-combo-box-item[disabled] {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
--md-list-item-disabled-opacity: 0.5;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
ha-combo-box-item {
|
||||
@@ -141,13 +143,6 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
--md-focus-ring-duration: 0s;
|
||||
}
|
||||
|
||||
/* Add Similar focus style as the text field */
|
||||
ha-combo-box-item[disabled]:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
ha-combo-box-item:after {
|
||||
display: block;
|
||||
content: "";
|
||||
|
||||
@@ -2,6 +2,7 @@ import memoizeOne from "memoize-one";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mdiChartBellCurveCumulative } from "@mdi/js";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
NumericThresholdSelector,
|
||||
@@ -76,6 +77,18 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
(changedProperties.has("value") || changedProperties.has("selector")) &&
|
||||
!this.value
|
||||
) {
|
||||
const mode = this._getMode();
|
||||
const type = DEFAULT_TYPE[mode];
|
||||
fireEvent(this, "value-changed", { value: { type } });
|
||||
}
|
||||
}
|
||||
|
||||
private _getUnitOptions() {
|
||||
return this.selector.numeric_threshold?.unit_of_measurement;
|
||||
}
|
||||
@@ -220,6 +233,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
return [
|
||||
{
|
||||
value: "any",
|
||||
iconPath: mdiChartBellCurveCumulative,
|
||||
label: localize(
|
||||
"ui.components.selectors.numeric_threshold.changed.any"
|
||||
),
|
||||
@@ -481,7 +495,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
.value-inputs {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: flex-end;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.value-selector {
|
||||
|
||||
+73
-57
@@ -1,9 +1,9 @@
|
||||
import "@home-assistant/webawesome/dist/components/popup/popup";
|
||||
import type WaPopup from "@home-assistant/webawesome/dist/components/popup/popup";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } 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 { popoverSupported } from "../common/feature-detect/support-popover";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
|
||||
export type ToastCloseReason =
|
||||
@@ -22,9 +22,6 @@ export class HaToast extends LitElement {
|
||||
|
||||
@property({ type: Number, attribute: "timeout-ms" }) public timeoutMs = 4000;
|
||||
|
||||
@query("wa-popup")
|
||||
private _popup?: WaPopup;
|
||||
|
||||
@query(".toast")
|
||||
private _toast?: HTMLDivElement;
|
||||
|
||||
@@ -48,7 +45,6 @@ export class HaToast extends LitElement {
|
||||
clearTimeout(this._dismissTimer);
|
||||
|
||||
if (this._active && this._visible) {
|
||||
this._popup?.reposition();
|
||||
this._setDismissTimer();
|
||||
return;
|
||||
}
|
||||
@@ -62,7 +58,7 @@ export class HaToast extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._popup?.reposition();
|
||||
this._showToastPopover();
|
||||
await nextRender();
|
||||
|
||||
if (transitionId !== this._transitionId) {
|
||||
@@ -102,6 +98,7 @@ export class HaToast extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._hideToastPopover();
|
||||
this._active = false;
|
||||
await this.updateComplete;
|
||||
|
||||
@@ -123,6 +120,34 @@ export class HaToast extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _isPopoverOpen(): boolean {
|
||||
if (!this._toast || !popoverSupported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return this._toast.matches(":popover-open");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _showToastPopover(): void {
|
||||
if (!this._toast || !popoverSupported || this._isPopoverOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toast.showPopover?.();
|
||||
}
|
||||
|
||||
private _hideToastPopover(): void {
|
||||
if (!this._toast || !popoverSupported || !this._isPopoverOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toast.hidePopover?.();
|
||||
}
|
||||
|
||||
private async _waitForTransitionEnd(): Promise<void> {
|
||||
const toastEl = this._toast;
|
||||
if (!toastEl) {
|
||||
@@ -139,59 +164,46 @@ export class HaToast extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<wa-popup
|
||||
placement="top"
|
||||
.active=${this._active}
|
||||
.distance=${16}
|
||||
skidding="0"
|
||||
flip
|
||||
shift
|
||||
<div
|
||||
class=${classMap({
|
||||
toast: true,
|
||||
active: this._active,
|
||||
visible: this._visible,
|
||||
})}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
|
||||
>
|
||||
<div id="toast-anchor" slot="anchor" aria-hidden="true"></div>
|
||||
<div
|
||||
class=${classMap({
|
||||
toast: true,
|
||||
visible: this._visible,
|
||||
})}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span class="message">${this.labelText}</span>
|
||||
<div class="actions">
|
||||
<slot name="action"></slot>
|
||||
<slot name="dismiss"></slot>
|
||||
</div>
|
||||
<span class="message">${this.labelText}</span>
|
||||
<div class="actions">
|
||||
<slot name="action"></slot>
|
||||
<slot name="dismiss"></slot>
|
||||
</div>
|
||||
</wa-popup>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
#toast-anchor {
|
||||
position: fixed;
|
||||
bottom: calc(var(--ha-space-2) + var(--safe-area-inset-bottom));
|
||||
inset-inline-start: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
wa-popup::part(popup) {
|
||||
padding: 0;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
inset-block-start: auto;
|
||||
inset-inline-end: auto;
|
||||
inset-block-end: calc(
|
||||
var(--safe-area-inset-bottom, 0px) + var(--ha-space-4)
|
||||
);
|
||||
inset-inline-start: 50%;
|
||||
margin: 0;
|
||||
width: max-content;
|
||||
height: auto;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
min-width: min(
|
||||
350px,
|
||||
calc(
|
||||
100vw - var(--ha-space-4) - var(--safe-area-inset-left) - var(
|
||||
--safe-area-inset-right
|
||||
100vw - var(--ha-space-4) - var(--safe-area-inset-left, 0px) - var(
|
||||
--safe-area-inset-right,
|
||||
0px
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -200,20 +212,25 @@ export class HaToast extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
padding: var(--ha-space-3);
|
||||
color: var(--ha-color-on-neutral-loud);
|
||||
background-color: var(--ha-color-neutral-10);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
opacity: 0;
|
||||
transform: translateY(var(--ha-space-2));
|
||||
transform: translate(-50%, var(--ha-space-2));
|
||||
transition:
|
||||
opacity var(--ha-animation-duration-fast, 150ms) ease,
|
||||
transform var(--ha-animation-duration-fast, 150ms) ease;
|
||||
}
|
||||
|
||||
.toast:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toast.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.message {
|
||||
@@ -229,13 +246,12 @@ export class HaToast extends LitElement {
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
wa-popup::part(popup) {
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: calc(
|
||||
100vw - var(--safe-area-inset-left) - var(--safe-area-inset-right)
|
||||
100vw - var(--safe-area-inset-left, 0px) - var(
|
||||
--safe-area-inset-right,
|
||||
0px
|
||||
)
|
||||
);
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
|
||||
@@ -620,6 +620,10 @@ export class HaInput extends LitElement {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
}
|
||||
|
||||
wa-input:disabled::part(label) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
wa-input::part(hint) {
|
||||
height: var(--ha-space-5);
|
||||
margin-block-start: 0;
|
||||
|
||||
@@ -535,7 +535,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
|
||||
const stateObject: HassEntity | undefined = this.hass.states[item];
|
||||
const entityName = stateObject
|
||||
? computeEntityName(stateObject, this.hass.entities)
|
||||
? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
|
||||
: item;
|
||||
const { area, device } = stateObject
|
||||
? getEntityContext(
|
||||
|
||||
@@ -15,7 +15,7 @@ interface HassioHardwareAudioList {
|
||||
};
|
||||
}
|
||||
|
||||
interface HardwareDevice {
|
||||
export interface HardwareDevice {
|
||||
attributes: Record<string, string>;
|
||||
by_id: null | string;
|
||||
dev_path: string;
|
||||
|
||||
@@ -73,7 +73,6 @@ class DialogBox extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
type=${confirmPrompt ? "alert" : "standard"}
|
||||
?prevent-scrim-close=${confirmPrompt}
|
||||
@@ -104,6 +103,9 @@ class DialogBox extends LitElement {
|
||||
: nothing}
|
||||
${dialogTitle}
|
||||
</span>
|
||||
${this._params.subtitle
|
||||
? html`<span slot="subtitle">${this._params.subtitle}</span>`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div id="dialog-box-description">
|
||||
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
|
||||
|
||||
@@ -5,6 +5,7 @@ interface BaseDialogBoxParams {
|
||||
confirmText?: string;
|
||||
text?: string | TemplateResult;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
warning?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -536,9 +536,9 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
: undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass.entities)
|
||||
? computeEntityName(stateObj, this.hass.entities, this.hass.devices)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry)
|
||||
? computeEntityEntryName(this._entry, this.hass.devices)
|
||||
: entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
|
||||
@@ -107,7 +107,11 @@ class MoreInfoContent extends LitElement {
|
||||
if (!stateObj) {
|
||||
return null;
|
||||
}
|
||||
const entityName = computeEntityName(stateObj, hass.entities);
|
||||
const entityName = computeEntityName(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices
|
||||
);
|
||||
const { area } = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
|
||||
@@ -271,6 +271,7 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
max-width: 300px;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -65,6 +65,9 @@ const processAreasForClimate = (
|
||||
if (temperatureEntityId && hass.states[temperatureEntityId]) {
|
||||
areaCards.push({
|
||||
...computeTileCard(temperatureEntityId),
|
||||
name:
|
||||
hass.localize("component.sensor.entity_component.temperature.name") ||
|
||||
"Temperature",
|
||||
features: [{ type: "trend-graph" }],
|
||||
});
|
||||
}
|
||||
@@ -73,6 +76,9 @@ const processAreasForClimate = (
|
||||
if (humidityEntityId && hass.states[humidityEntityId]) {
|
||||
areaCards.push({
|
||||
...computeTileCard(humidityEntityId),
|
||||
name:
|
||||
hass.localize("component.sensor.entity_component.humidity.name") ||
|
||||
"Humidity",
|
||||
features: [{ type: "trend-graph" }],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -486,17 +486,6 @@ class SupervisorAppInfo extends LitElement {
|
||||
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.visit_app_page",
|
||||
{
|
||||
name: html`<a
|
||||
href=${this.addon.url!}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${getAppDisplayName(this.addon.name, this.addon.stage)}</a
|
||||
>`,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div class="addon-container">
|
||||
<div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import type { HassioAddonDetails } from "../../../data/hassio/addon";
|
||||
import { fetchHassioAddonInfo } from "../../../data/hassio/addon";
|
||||
@@ -234,7 +234,7 @@ class HaConfigAppDashboard extends LitElement {
|
||||
|
||||
if (path === "uninstall") {
|
||||
// Navigate back to installed apps after uninstall
|
||||
window.history.back();
|
||||
goBack(this._fromStore ? "/config/apps/available" : "/config/apps");
|
||||
} else {
|
||||
// Reload app info
|
||||
await this._loadAddon();
|
||||
|
||||
@@ -185,6 +185,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
@state() private _collapsed = true;
|
||||
|
||||
@state() private _isNew = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query("ha-automation-action-editor")
|
||||
@@ -237,12 +239,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
private _renderRow() {
|
||||
const type = getAutomationActionType(this.action);
|
||||
|
||||
const target =
|
||||
type === "service" && "target" in this.action
|
||||
? (this.action as ServiceAction).target
|
||||
: type === "device_id" && (this.action as DeviceAction).device_id
|
||||
? { device_id: (this.action as DeviceAction).device_id }
|
||||
: undefined;
|
||||
const actionHasTarget = type === "service" && "target" in this.action;
|
||||
|
||||
const target = actionHasTarget
|
||||
? (this.action as ServiceAction).target
|
||||
: type === "device_id" && (this.action as DeviceAction).device_id
|
||||
? { device_id: (this.action as DeviceAction).device_id }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${type === "service" && "action" in this.action && this.action.action
|
||||
@@ -265,7 +268,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
)}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
${target !== undefined || (actionHasTarget && !this._isNew)
|
||||
? this._renderTargets(target, actionHasTarget && !this._isNew)
|
||||
: nothing}
|
||||
${type !== "condition" &&
|
||||
(this.action as NonConditionAction).continue_on_error === true
|
||||
? html`<ha-svg-icon
|
||||
@@ -575,10 +580,11 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
(target?: HassServiceTarget) =>
|
||||
(target?: HassServiceTarget, targetRequired = false) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
@@ -812,6 +818,10 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this.openSidebar();
|
||||
}
|
||||
|
||||
public markAsNew(): void {
|
||||
this._isNew = true;
|
||||
}
|
||||
|
||||
public openSidebar(action?: Action): void {
|
||||
const sidebarAction = action ?? this.action;
|
||||
const actionType = getAutomationActionType(sidebarAction);
|
||||
@@ -822,6 +832,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
},
|
||||
close: (focus?: boolean) => {
|
||||
this._selected = false;
|
||||
this._isNew = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
if (focus) {
|
||||
this.focus();
|
||||
@@ -901,6 +912,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
);
|
||||
|
||||
private _toggleCollapse() {
|
||||
if (!this._collapsed) {
|
||||
this._isNew = false;
|
||||
}
|
||||
this._collapsed = !this._collapsed;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ export default class HaAutomationAction extends AutomationSortableListMixin<Acti
|
||||
|
||||
if (mode === "new") {
|
||||
row.expand();
|
||||
row.markAsNew();
|
||||
}
|
||||
|
||||
if (!this.optionsInSidebar) {
|
||||
|
||||
@@ -503,7 +503,7 @@ class DialogAddAutomationElement
|
||||
const tabButtons = [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.name`
|
||||
"ui.panel.config.automation.editor.tabs.type"
|
||||
),
|
||||
value: "groups",
|
||||
},
|
||||
@@ -511,7 +511,9 @@ class DialogAddAutomationElement
|
||||
|
||||
if (this._newTriggersAndConditions) {
|
||||
tabButtons.unshift({
|
||||
label: this.hass.localize(`ui.panel.config.automation.editor.targets`),
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.tabs.target"
|
||||
),
|
||||
value: "targets",
|
||||
});
|
||||
}
|
||||
@@ -610,10 +612,10 @@ class DialogAddAutomationElement
|
||||
.narrow=${this._narrow}
|
||||
class=${classMap({
|
||||
"ha-scrollbar": true,
|
||||
[this._getAddFromTargetHidden(
|
||||
hidden: !!this._getAddFromTargetHidden(
|
||||
this._narrow,
|
||||
this._selectedTarget
|
||||
)]: true,
|
||||
),
|
||||
})}
|
||||
.manifests=${this._manifests}
|
||||
></ha-automation-add-from-target>`
|
||||
|
||||
@@ -107,6 +107,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
@state() private _collapsed = true;
|
||||
|
||||
@state() private _isNew = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -160,13 +162,15 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
const target =
|
||||
"target" in (this.conditionDescriptions[this.condition.condition] || {})
|
||||
? (this.condition as PlatformCondition).target
|
||||
: "device_id" in this.condition &&
|
||||
(this.condition as DeviceCondition).device_id
|
||||
? { device_id: [(this.condition as DeviceCondition).device_id] }
|
||||
: undefined;
|
||||
const descriptionHasTarget =
|
||||
"target" in (this.conditionDescriptions[this.condition.condition] || {});
|
||||
|
||||
const target = descriptionHasTarget
|
||||
? (this.condition as PlatformCondition).target
|
||||
: "device_id" in this.condition &&
|
||||
(this.condition as DeviceCondition).device_id
|
||||
? { device_id: [(this.condition as DeviceCondition).device_id] }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-condition-icon
|
||||
@@ -178,7 +182,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
${target !== undefined || (descriptionHasTarget && !this._isNew)
|
||||
? this._renderTargets(target, descriptionHasTarget && !this._isNew)
|
||||
: nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -464,10 +470,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
(target?: HassServiceTarget) =>
|
||||
(target?: HassServiceTarget, targetRequired = false) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
@@ -743,6 +750,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this.openSidebar();
|
||||
}
|
||||
|
||||
public markAsNew(): void {
|
||||
this._isNew = true;
|
||||
}
|
||||
|
||||
public openSidebar(condition?: Condition): void {
|
||||
const sidebarCondition = condition || this.condition;
|
||||
fireEvent(this, "open-sidebar", {
|
||||
@@ -751,6 +762,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
},
|
||||
close: (focus?: boolean) => {
|
||||
this._selected = false;
|
||||
this._isNew = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
if (focus) {
|
||||
this.focus();
|
||||
@@ -806,6 +818,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
);
|
||||
|
||||
private _toggleCollapse() {
|
||||
if (!this._collapsed) {
|
||||
this._isNew = false;
|
||||
}
|
||||
this._collapsed = !this._collapsed;
|
||||
}
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
|
||||
|
||||
if (mode === "new") {
|
||||
row.expand();
|
||||
row.markAsNew();
|
||||
}
|
||||
|
||||
if (!this.optionsInSidebar) {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlert,
|
||||
mdiAlertOctagon,
|
||||
mdiCodeBraces,
|
||||
mdiFormatListBulleted,
|
||||
mdiShape,
|
||||
} from "@mdi/js";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, type nothing, type TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing, type TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { transform } from "../../../../common/decorators/transform";
|
||||
import { isTemplate } from "../../../../common/string/has-template";
|
||||
@@ -35,6 +37,9 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public target?: HassServiceTarget;
|
||||
|
||||
@property({ attribute: false })
|
||||
public targetRequired = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: HomeAssistant["localize"];
|
||||
@@ -73,13 +78,16 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
protected render() {
|
||||
const length = Object.keys(this.target || {}).length;
|
||||
if (!length) {
|
||||
return html`<span class="target">
|
||||
<div class="label">
|
||||
${this.localize(
|
||||
"ui.panel.config.automation.editor.target_summary.no_target"
|
||||
)}
|
||||
</div>
|
||||
</span>`;
|
||||
return this._renderTargetBadge(
|
||||
this.targetRequired
|
||||
? html`<ha-svg-icon .path=${mdiAlertOctagon}></ha-svg-icon>`
|
||||
: nothing,
|
||||
this.localize(
|
||||
"ui.panel.config.automation.editor.target_summary.no_target"
|
||||
),
|
||||
false,
|
||||
this.targetRequired
|
||||
);
|
||||
}
|
||||
const totalLength = Object.values(this.target || {}).reduce(
|
||||
(acc, val) => acc + ensureArray(val).length,
|
||||
@@ -154,9 +162,10 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
private _renderTargetBadge(
|
||||
icon: TemplateResult | typeof nothing,
|
||||
label: string,
|
||||
alert = false
|
||||
warning = false,
|
||||
error = false
|
||||
) {
|
||||
return html`<div class="target ${alert ? "alert" : ""}">
|
||||
return html`<div class=${classMap({ target: true, warning, error })}>
|
||||
${icon}
|
||||
<div class="label">${label}</div>
|
||||
</div>`;
|
||||
@@ -230,10 +239,14 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
overflow: hidden;
|
||||
height: 32px;
|
||||
}
|
||||
.target.alert {
|
||||
.target.warning {
|
||||
background: var(--ha-color-fill-warning-normal-resting);
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
.target.error {
|
||||
background: var(--ha-color-fill-danger-normal-resting);
|
||||
color: var(--ha-color-on-danger-normal);
|
||||
}
|
||||
.target .label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -145,6 +145,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
@state() private _selected = false;
|
||||
|
||||
@state() private _isNew = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -197,14 +199,16 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
const yamlMode = this._yamlMode || !supported;
|
||||
|
||||
const target =
|
||||
const descriptionHasTarget =
|
||||
type === "platform" &&
|
||||
"target" in
|
||||
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
|
||||
? (this.trigger as PlatformTrigger).target
|
||||
: type === "device" && (this.trigger as DeviceTrigger).device_id
|
||||
? { device_id: (this.trigger as DeviceTrigger).device_id }
|
||||
: undefined;
|
||||
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger];
|
||||
|
||||
const target = descriptionHasTarget
|
||||
? (this.trigger as PlatformTrigger).target
|
||||
: type === "device" && (this.trigger as DeviceTrigger).device_id
|
||||
? { device_id: (this.trigger as DeviceTrigger).device_id }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${type === "list"
|
||||
@@ -220,7 +224,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
></ha-trigger-icon>`}
|
||||
<h3 slot="header">
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
${target !== undefined || (descriptionHasTarget && !this._isNew)
|
||||
? this._renderTargets(target, descriptionHasTarget && !this._isNew)
|
||||
: nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -446,7 +452,10 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
: nothing}${this._renderRow()}</ha-automation-row
|
||||
>`
|
||||
: html`
|
||||
<ha-expansion-panel left-chevron>
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
@expanded-changed=${this._expansionPanelChanged}
|
||||
>
|
||||
${this._renderRow()}
|
||||
</ha-expansion-panel>
|
||||
`}
|
||||
@@ -467,10 +476,11 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
(target?: HassServiceTarget) =>
|
||||
(target?: HassServiceTarget, targetRequired = false) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
@@ -576,6 +586,16 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
this.openSidebar();
|
||||
}
|
||||
|
||||
public markAsNew(): void {
|
||||
this._isNew = true;
|
||||
}
|
||||
|
||||
private _expansionPanelChanged(ev: CustomEvent) {
|
||||
if (!ev.detail.expanded) {
|
||||
this._isNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
public openSidebar(trigger?: Trigger): void {
|
||||
trigger = trigger || this.trigger;
|
||||
fireEvent(this, "open-sidebar", {
|
||||
@@ -584,6 +604,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
},
|
||||
close: (focus?: boolean) => {
|
||||
this._selected = false;
|
||||
this._isNew = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
if (focus) {
|
||||
this.focus();
|
||||
|
||||
@@ -252,6 +252,7 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
row.expand();
|
||||
row.focus();
|
||||
}
|
||||
row.markAsNew();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
computeSection,
|
||||
} from "../../../lovelace/common/generate-lovelace-config";
|
||||
import { addEntitiesToLovelaceView } from "../../../lovelace/editor/add-entities-to-view";
|
||||
import type { EntityRegistryEntryWithDisplayName } from "../ha-config-device-page";
|
||||
import type { EntityRegistryStateEntry } from "../ha-config-device-page";
|
||||
import { entityRowElement } from "../../../lovelace/entity-rows/entity-row-element-directive";
|
||||
|
||||
@customElement("ha-device-entities-card")
|
||||
@@ -30,7 +30,7 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entities!: EntityRegistryEntryWithDisplayName[];
|
||||
public entities!: EntityRegistryStateEntry[];
|
||||
|
||||
@property({ attribute: "show-hidden", type: Boolean })
|
||||
public showHidden = false;
|
||||
@@ -115,10 +115,8 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
this.showHidden = !this.showHidden;
|
||||
}
|
||||
|
||||
private _renderEntity(
|
||||
entry: EntityRegistryEntryWithDisplayName
|
||||
): TemplateResult {
|
||||
let name = entry.display_name || this.deviceName;
|
||||
private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult {
|
||||
let name = entry.stateName || this.deviceName;
|
||||
if (entry.hidden_by) {
|
||||
name += ` (${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.hidden"
|
||||
@@ -130,9 +128,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
}
|
||||
|
||||
private _renderUnavailableEntity(
|
||||
entry: EntityRegistryEntryWithDisplayName
|
||||
entry: EntityRegistryStateEntry
|
||||
): TemplateResult {
|
||||
const name = entry.display_name || this.deviceName;
|
||||
const name = entry.stateName || this.deviceName;
|
||||
|
||||
const icon = until(entryIcon(this.hass, entry));
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ import "../../../components/entity/ha-battery-icon";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
@@ -66,6 +65,7 @@ import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import {
|
||||
findBatteryChargingEntity,
|
||||
findBatteryEntity,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
@@ -94,9 +94,10 @@ import {
|
||||
loadDeviceRegistryDetailDialog,
|
||||
showDeviceRegistryDetailDialog,
|
||||
} from "./device-registry-detail/show-dialog-device-registry-detail";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
export interface EntityRegistryEntryWithDisplayName extends EntityRegistryEntry {
|
||||
display_name?: string | null;
|
||||
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
|
||||
stateName?: string | null;
|
||||
}
|
||||
|
||||
export interface DeviceAction {
|
||||
@@ -176,17 +177,17 @@ export class HaConfigDevicePage extends LitElement {
|
||||
(
|
||||
deviceId: string,
|
||||
entities: EntityRegistryEntry[]
|
||||
): EntityRegistryEntry[] =>
|
||||
): EntityRegistryStateEntry[] =>
|
||||
entities
|
||||
.filter((entity) => entity.device_id === deviceId)
|
||||
.map((entity) => ({
|
||||
...entity,
|
||||
display_name: computeEntityEntryName(entity),
|
||||
stateName: this._computeEntityName(entity),
|
||||
}))
|
||||
.sort((ent1, ent2) =>
|
||||
stringCompare(
|
||||
ent1.display_name || "",
|
||||
ent2.display_name || "",
|
||||
ent1.stateName || `zzz${ent1.entity_id}`,
|
||||
ent2.stateName || `zzz${ent2.entity_id}`,
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
@@ -216,7 +217,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
private _deviceIdInList = memoizeOne((deviceId: string) => [deviceId]);
|
||||
|
||||
private _entityIds = memoizeOne(
|
||||
(entries: EntityRegistryEntryWithDisplayName[]): string[] =>
|
||||
(entries: EntityRegistryStateEntry[]): string[] =>
|
||||
entries.map((entry) => entry.entity_id)
|
||||
);
|
||||
|
||||
@@ -249,7 +250,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
| "assist"
|
||||
| "notify"
|
||||
| NonNullable<EntityRegistryEntry["entity_category"]>,
|
||||
EntityRegistryEntryWithDisplayName[]
|
||||
EntityRegistryStateEntry[]
|
||||
>;
|
||||
for (const key of [
|
||||
"assist",
|
||||
@@ -1270,6 +1271,14 @@ export class HaConfigDevicePage extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _computeEntityName(entity: EntityRegistryEntry) {
|
||||
const device = this.hass.devices[this.deviceId];
|
||||
return (
|
||||
computeEntityEntryName(entity, this.hass.devices) ||
|
||||
computeDeviceNameDisplay(device, this.hass)
|
||||
);
|
||||
}
|
||||
|
||||
private _onImageLoad(ev) {
|
||||
ev.target.style.display = "inline-block";
|
||||
}
|
||||
@@ -1362,6 +1371,8 @@ export class HaConfigDevicePage extends LitElement {
|
||||
showDeviceRegistryDetailDialog(this, {
|
||||
device,
|
||||
updateEntry: async (updates) => {
|
||||
const oldDeviceName = device.name_by_user || device.name;
|
||||
const newDeviceName = updates.name_by_user;
|
||||
const disabled =
|
||||
updates.disabled_by === "user" && device.disabled_by !== "user";
|
||||
|
||||
@@ -1433,7 +1444,43 @@ export class HaConfigDevicePage extends LitElement {
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!oldDeviceName ||
|
||||
!newDeviceName ||
|
||||
oldDeviceName === newDeviceName
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const entities = this._entities(this.deviceId, this._entityReg);
|
||||
|
||||
const updateProms = entities.map((entity) => {
|
||||
const name = entity.name || entity.stateName;
|
||||
let newName: string | null | undefined;
|
||||
|
||||
if (entity.has_entity_name && !entity.name) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
entity.has_entity_name &&
|
||||
(entity.name === oldDeviceName || entity.name === newDeviceName)
|
||||
) {
|
||||
// clear name if it matches the device name and it uses the device name (entity naming)
|
||||
newName = null;
|
||||
} else if (name && name.includes(oldDeviceName)) {
|
||||
newName = name.replace(oldDeviceName, newDeviceName);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
|
||||
name: newName,
|
||||
});
|
||||
});
|
||||
await Promise.all(updateProms);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -140,9 +140,9 @@ export class DialogVacuumSegmentMapping
|
||||
: undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass.entities)
|
||||
? computeEntityName(stateObj, this.hass.entities, this.hass.devices)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry)
|
||||
? computeEntityEntryName(this._entry, this.hass.devices)
|
||||
: this._params.entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
|
||||
@@ -164,7 +164,7 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
}
|
||||
|
||||
private async _confirmDeleteItem(): Promise<void> {
|
||||
const name = computeEntityEntryName(this.entry);
|
||||
const name = computeEntityEntryName(this.entry, this.hass.devices);
|
||||
const confirmationText = await getDeleteConfirmationText(
|
||||
this.hass,
|
||||
this.entry,
|
||||
|
||||
@@ -153,7 +153,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public helperConfigEntry?: ConfigEntry;
|
||||
|
||||
@state() private _name!: string | null;
|
||||
@state() private _name!: string;
|
||||
|
||||
@state() private _icon!: string;
|
||||
|
||||
@@ -218,7 +218,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._name = this.entry.name;
|
||||
this._name = this.entry.name || "";
|
||||
this._icon = this.entry.icon || "";
|
||||
this._deviceClass =
|
||||
this.entry.device_class || this.entry.original_device_class;
|
||||
@@ -388,27 +388,14 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
${this.hideName
|
||||
? nothing
|
||||
: html`<ha-textfield
|
||||
class="name"
|
||||
.value=${this._name ?? this.entry.original_name ?? ""}
|
||||
.value=${this._name}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.name"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
.placeholder=${this.entry.original_name}
|
||||
@input=${this._nameChanged}
|
||||
.iconTrailing=${this._name !== null}
|
||||
>
|
||||
${this._name !== null
|
||||
? html`<div class="layout horizontal" slot="trailingIcon">
|
||||
<ha-icon-button
|
||||
@click=${this._restoreName}
|
||||
.path=${mdiRestore}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.restore_name"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-textfield>`}
|
||||
></ha-textfield>`}
|
||||
${this.hideIcon
|
||||
? nothing
|
||||
: html`
|
||||
@@ -1065,7 +1052,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
const params: Partial<EntityRegistryEntryUpdateParams> = {
|
||||
name: this._name?.trim() ?? null,
|
||||
name: this._name.trim() || null,
|
||||
icon: this._icon.trim() || null,
|
||||
area_id: this._areaId || null,
|
||||
labels: this._labels || [],
|
||||
@@ -1331,11 +1318,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._name = ev.target.value;
|
||||
}
|
||||
|
||||
private _restoreName(): void {
|
||||
fireEvent(this, "change");
|
||||
this._name = null;
|
||||
}
|
||||
|
||||
private _iconChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._icon = ev.detail.value;
|
||||
@@ -1610,13 +1592,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
ha-textfield.entityId {
|
||||
--text-field-prefix-padding-right: 0;
|
||||
}
|
||||
ha-textfield.entityId,
|
||||
ha-textfield.name {
|
||||
--textfield-icon-trailing-padding: 0;
|
||||
}
|
||||
ha-textfield.entityId ha-icon-button,
|
||||
ha-textfield.name ha-icon-button {
|
||||
ha-textfield.entityId ha-icon-button {
|
||||
position: relative;
|
||||
right: calc(var(--ha-space-2) * -1);
|
||||
--ha-icon-button-size: 36px;
|
||||
|
||||
@@ -213,7 +213,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _confirmDeleteEntry(): Promise<void> {
|
||||
let name = computeEntityEntryName(this.entry);
|
||||
let name = computeEntityEntryName(this.entry, this.hass.devices);
|
||||
if (!name) {
|
||||
const { device } = getEntityEntryContext(
|
||||
this.entry,
|
||||
|
||||
@@ -32,10 +32,7 @@ import {
|
||||
getDuplicatedDeviceNames,
|
||||
} from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import {
|
||||
computeEntityEntryName,
|
||||
computeEntityName,
|
||||
} from "../../../common/entity/compute_entity_name";
|
||||
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import {
|
||||
deleteEntity,
|
||||
@@ -693,9 +690,11 @@ export class HaConfigEntities extends LitElement {
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
);
|
||||
|
||||
const entityName = entity
|
||||
? computeEntityName(entity, this.hass.entities)
|
||||
: computeEntityEntryName(entry as EntityRegistryEntry);
|
||||
const entityName = computeEntityEntryName(
|
||||
entry as EntityRegistryEntry,
|
||||
this.hass.devices,
|
||||
entity
|
||||
);
|
||||
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../components/input/ha-input-search";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type { HassioHardwareInfo } from "../../../data/hassio/hardware";
|
||||
import { fetchHassioHardwareInfo } from "../../../data/hassio/hardware";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { haStyleScrollbar } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
const _filterDevices = memoizeOne(
|
||||
(
|
||||
showAdvanced: boolean,
|
||||
hardware: HassioHardwareInfo,
|
||||
filter: string,
|
||||
language: string
|
||||
) =>
|
||||
hardware.devices
|
||||
.filter(
|
||||
(device) =>
|
||||
(showAdvanced ||
|
||||
["tty", "gpio", "input"].includes(device.subsystem)) &&
|
||||
(device.by_id?.toLowerCase().includes(filter) ||
|
||||
device.name.toLowerCase().includes(filter) ||
|
||||
device.dev_path.toLocaleLowerCase().includes(filter) ||
|
||||
JSON.stringify(device.attributes)
|
||||
.toLocaleLowerCase()
|
||||
.includes(filter))
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
);
|
||||
|
||||
@customElement("ha-dialog-hardware-available")
|
||||
class DialogHardwareAvailable extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _hardware?: HassioHardwareInfo;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(): Promise<Promise<void>> {
|
||||
try {
|
||||
this._hardware = await fetchHassioHardwareInfo(this.hass);
|
||||
this._open = true;
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.failed_to_get"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._open = false;
|
||||
this._hardware = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._hardware) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const devices = _filterDevices(
|
||||
this.hass.userData?.showAdvanced || false,
|
||||
this._hardware,
|
||||
(this._filter || "").toLowerCase(),
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.title"
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div class="content-container">
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
autofocus
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
.placeholder=${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.search"
|
||||
)}
|
||||
>
|
||||
</ha-input-search>
|
||||
<div class="devices-container ha-scrollbar">
|
||||
${devices.map(
|
||||
(device) => html`
|
||||
<ha-expansion-panel
|
||||
.header=${device.name}
|
||||
.secondary=${device.by_id || undefined}
|
||||
outlined
|
||||
>
|
||||
<div class="device-property">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.subsystem"
|
||||
)}:
|
||||
</span>
|
||||
<span>${device.subsystem}</span>
|
||||
</div>
|
||||
<div class="device-property">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.device_path"
|
||||
)}:
|
||||
</span>
|
||||
<code>${device.dev_path}</code>
|
||||
</div>
|
||||
${device.by_id
|
||||
? html`
|
||||
<div class="device-property">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.id"
|
||||
)}:
|
||||
</span>
|
||||
<code>${device.by_id}</code>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="attributes">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.attributes"
|
||||
)}:
|
||||
</span>
|
||||
<pre>${dump(device.attributes, { indent: 2 })}</pre>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
this._filter = (ev.target as HaInputSearch).value ?? "";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
.content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.devices-container {
|
||||
padding: var(--ha-space-6);
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
flex: 1;
|
||||
margin: 4px 0;
|
||||
}
|
||||
pre,
|
||||
code {
|
||||
background-color: var(--markdown-code-background-color, none);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-family: var(--ha-font-family-code);
|
||||
}
|
||||
code {
|
||||
font-size: var(--ha-font-size-s);
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
ha-input-search {
|
||||
padding: 0 var(--ha-space-5);
|
||||
}
|
||||
.device-property {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.attributes {
|
||||
margin-top: 12px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog-hardware-available": DialogHardwareAvailable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/data-table/ha-data-table";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableRowData,
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type {
|
||||
HardwareDevice,
|
||||
HassioHardwareInfo,
|
||||
} from "../../../data/hassio/hardware";
|
||||
import { fetchHassioHardwareInfo } from "../../../data/hassio/hardware";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { hardwareTabs } from "./ha-config-hardware";
|
||||
|
||||
interface HardwareDeviceRow extends HardwareDevice {
|
||||
id: string;
|
||||
attributes_string: string;
|
||||
}
|
||||
|
||||
@customElement("ha-config-hardware-all")
|
||||
class HaConfigHardwareAll extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _hardware?: HassioHardwareInfo;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(localize: LocalizeFunc): DataTableColumnContainer<HardwareDeviceRow> => ({
|
||||
name: {
|
||||
title: localize("ui.panel.config.hardware.system_hardware.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
flex: 2,
|
||||
},
|
||||
dev_path: {
|
||||
title: localize("ui.panel.config.hardware.system_hardware.device_path"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
flex: 2,
|
||||
},
|
||||
by_id: {
|
||||
title: localize("ui.panel.config.hardware.system_hardware.id"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
flex: 2,
|
||||
},
|
||||
subsystem: {
|
||||
title: localize("ui.panel.config.hardware.system_hardware.subsystem"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
flex: 1,
|
||||
},
|
||||
attributes_string: {
|
||||
title: "",
|
||||
filterable: true,
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
private _data = memoizeOne(
|
||||
(showAdvanced: boolean, hardware: HassioHardwareInfo): DataTableRowData[] =>
|
||||
hardware.devices
|
||||
.filter(
|
||||
(device) =>
|
||||
showAdvanced || ["tty", "gpio", "input"].includes(device.subsystem)
|
||||
)
|
||||
.map((device) => ({
|
||||
...device,
|
||||
id: device.dev_path,
|
||||
attributes_string: Object.entries(device.attributes)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(" "),
|
||||
}))
|
||||
);
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._load();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/system"
|
||||
.route=${this.route}
|
||||
.tabs=${hardwareTabs(this.hass)}
|
||||
clickable
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._hardware
|
||||
? this._data(
|
||||
this.hass.userData?.showAdvanced || false,
|
||||
this._hardware
|
||||
)
|
||||
: []}
|
||||
.noDataText=${this._error ||
|
||||
this.hass.localize("ui.panel.config.hardware.loading_system_data")}
|
||||
@row-click=${this._handleRowClicked}
|
||||
></hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _load() {
|
||||
try {
|
||||
this._hardware = await fetchHassioHardwareInfo(this.hass);
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: CustomEvent<RowClickedEvent>) {
|
||||
const id = ev.detail.id;
|
||||
const device = this._hardware?.devices.find((dev) => dev.dev_path === id);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
showAlertDialog(this, {
|
||||
title: device.name,
|
||||
subtitle: this.hass.localize(
|
||||
"ui.panel.config.hardware.system_hardware.attributes"
|
||||
),
|
||||
text: html`${until(this._renderHaCodeEditor(device))}`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _renderHaCodeEditor(device: HardwareDevice) {
|
||||
await import("../../../components/ha-code-editor");
|
||||
|
||||
return html`<ha-code-editor
|
||||
mode="yaml"
|
||||
.hass=${this.hass}
|
||||
.value=${dump(device.attributes, { indent: 2 })}
|
||||
read-only
|
||||
></ha-code-editor>`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
ha-code-editor {
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-hardware-all": HaConfigHardwareAll;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
import { mdiPower } from "@mdi/js";
|
||||
import type { SeriesOption } from "echarts/types/dist/shared";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { round } from "../../../common/number/round";
|
||||
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
|
||||
import "../../../components/chart/ha-chart-base";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fade-in";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { subscribeConfigEntries } from "../../../data/config_entries";
|
||||
import type {
|
||||
HardwareInfo,
|
||||
SystemStatusStreamMessage,
|
||||
} from "../../../data/hardware";
|
||||
import { BOARD_NAMES } from "../../../data/hardware";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type { HassioHassOSInfo } from "../../../data/hassio/host";
|
||||
import { fetchHassioHassOsInfo } from "../../../data/hassio/host";
|
||||
import { scanUSBDevices } from "../../../data/usb";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { ECOption } from "../../../resources/echarts/echarts";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { DefaultPrimaryColor } from "../../../resources/theme/color/color.globals";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { hardwareBrandsUrl } from "../../../util/brands-url";
|
||||
import { hardwareTabs } from "./ha-config-hardware";
|
||||
|
||||
const DATASAMPLES = 60;
|
||||
|
||||
const DATA_SET_CONFIG: SeriesOption = {
|
||||
type: "line",
|
||||
color: DefaultPrimaryColor,
|
||||
areaStyle: {
|
||||
color: DefaultPrimaryColor + "2B",
|
||||
},
|
||||
symbolSize: 0,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
smooth: 0.25,
|
||||
};
|
||||
|
||||
@customElement("ha-config-hardware-overview")
|
||||
class HaConfigHardwareOverview extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _OSData?: HassioHassOSInfo;
|
||||
|
||||
@state() private _hardwareInfo?: HardwareInfo;
|
||||
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
@state() private _systemStatusData?: SystemStatusStreamMessage;
|
||||
|
||||
@state() private _configEntries?: Record<string, ConfigEntry>;
|
||||
|
||||
private _memoryEntries: [number, number | null][] = [];
|
||||
|
||||
private _cpuEntries: [number, number | null][] = [];
|
||||
|
||||
public hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
const subs = [
|
||||
subscribeConfigEntries(
|
||||
this.hass,
|
||||
(messages) => {
|
||||
let fullUpdate = false;
|
||||
const newEntries: ConfigEntry[] = [];
|
||||
messages.forEach((message) => {
|
||||
if (message.type === null || message.type === "added") {
|
||||
newEntries.push(message.entry);
|
||||
if (message.type === null) {
|
||||
fullUpdate = true;
|
||||
}
|
||||
} else if (message.type === "removed") {
|
||||
if (this._configEntries) {
|
||||
delete this._configEntries[message.entry.entry_id];
|
||||
}
|
||||
} else if (message.type === "updated") {
|
||||
if (this._configEntries) {
|
||||
const newEntry = message.entry;
|
||||
this._configEntries[message.entry.entry_id] = newEntry;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!newEntries.length && !fullUpdate) {
|
||||
return;
|
||||
}
|
||||
const entries = [
|
||||
...(fullUpdate ? [] : Object.values(this._configEntries || {})),
|
||||
...newEntries,
|
||||
];
|
||||
const configEntries: Record<string, ConfigEntry> = {};
|
||||
for (const entry of entries) {
|
||||
configEntries[entry.entry_id] = entry;
|
||||
}
|
||||
this._configEntries = configEntries;
|
||||
},
|
||||
{ type: ["hardware"] }
|
||||
),
|
||||
];
|
||||
|
||||
if (isComponentLoaded(this.hass, "hardware")) {
|
||||
subs.push(
|
||||
this.hass.connection.subscribeMessage<SystemStatusStreamMessage>(
|
||||
(message) => {
|
||||
// Only store the last 60 entries
|
||||
this._memoryEntries.shift();
|
||||
this._cpuEntries.shift();
|
||||
|
||||
this._memoryEntries.push([
|
||||
new Date(message.timestamp).getTime(),
|
||||
message.memory_used_percent,
|
||||
]);
|
||||
this._cpuEntries.push([
|
||||
new Date(message.timestamp).getTime(),
|
||||
message.cpu_percent,
|
||||
]);
|
||||
|
||||
this._systemStatusData = message;
|
||||
},
|
||||
{
|
||||
type: "hardware/subscribe_system_status",
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return subs;
|
||||
}
|
||||
|
||||
protected willUpdate(): void {
|
||||
if (!this.hasUpdated && !this._chartOptions) {
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitLine: {
|
||||
show: true,
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: (value: number) =>
|
||||
value + blankBeforePercent(this.hass.locale) + "%",
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
scale: true,
|
||||
},
|
||||
grid: {
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
right: 10,
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
valueFormatter: (value) =>
|
||||
value + blankBeforePercent(this.hass.locale) + "%",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._load();
|
||||
|
||||
const date = new Date();
|
||||
// Force graph to start drawing from the right
|
||||
for (let i = 0; i < DATASAMPLES; i++) {
|
||||
const t = new Date(date);
|
||||
t.setSeconds(t.getSeconds() - 5 * (DATASAMPLES - i));
|
||||
this._memoryEntries.push([t.getTime(), null]);
|
||||
this._cpuEntries.push([t.getTime(), null]);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
let boardId: string | undefined;
|
||||
let boardName: string | undefined;
|
||||
let imageURL: string | undefined;
|
||||
let documentationURL: string | undefined;
|
||||
let boardConfigEntries: ConfigEntry[] = [];
|
||||
|
||||
const boardData = this._hardwareInfo?.hardware.find(
|
||||
(hw) => hw.board !== null
|
||||
);
|
||||
|
||||
const dongles = this._hardwareInfo?.hardware.filter(
|
||||
(hw) =>
|
||||
hw.dongle !== null &&
|
||||
(!hw.config_entries.length ||
|
||||
hw.config_entries.some(
|
||||
(entryId) =>
|
||||
this._configEntries?.[entryId] &&
|
||||
!this._configEntries[entryId].disabled_by
|
||||
))
|
||||
);
|
||||
|
||||
if (boardData) {
|
||||
boardConfigEntries = boardData.config_entries
|
||||
.map((id) => this._configEntries?.[id])
|
||||
.filter(
|
||||
(entry) => entry?.supports_options && !entry.disabled_by
|
||||
) as ConfigEntry[];
|
||||
boardId = boardData.board!.hassio_board_id;
|
||||
boardName = boardData.name;
|
||||
documentationURL = boardData.url;
|
||||
imageURL = hardwareBrandsUrl(
|
||||
{
|
||||
category: "boards",
|
||||
manufacturer: boardData.board!.manufacturer,
|
||||
model: boardData.board!.model,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
);
|
||||
} else if (this._OSData?.board) {
|
||||
boardId = this._OSData.board;
|
||||
boardName = BOARD_NAMES[this._OSData.board];
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${hardwareTabs(this.hass)}
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio")
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiPower}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.hardware.restart_homeassistant"
|
||||
)}
|
||||
@click=${this._showRestartDialog}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<div class="content">
|
||||
${boardName || isComponentLoaded(this.hass, "hassio")
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${imageURL
|
||||
? html`<img
|
||||
alt=""
|
||||
src=${imageURL}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>`
|
||||
: nothing}
|
||||
<div class="board-info">
|
||||
<p class="primary-text">
|
||||
${boardName ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.hardware.generic_hardware"
|
||||
)}
|
||||
</p>
|
||||
${boardId
|
||||
? html`<p class="secondary-text">${boardId}</p>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
${documentationURL
|
||||
? html`
|
||||
<ha-md-list-item
|
||||
.href=${documentationURL}
|
||||
type="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.hardware.documentation"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.hardware.documentation_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${boardConfigEntries.length
|
||||
? html`<div class="card-actions">
|
||||
<ha-button
|
||||
.entry=${boardConfigEntries[0]}
|
||||
@click=${this._openOptionsFlow}
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.configure"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
${dongles?.length
|
||||
? html`<ha-card outlined>
|
||||
${dongles.map((dongle) => {
|
||||
const configEntry = dongle.config_entries
|
||||
.map((id) => this._configEntries?.[id])
|
||||
.filter(
|
||||
(entry) => entry?.supports_options && !entry.disabled_by
|
||||
)[0];
|
||||
return html`<div class="row">
|
||||
${dongle.name}${configEntry
|
||||
? html`<ha-button
|
||||
.entry=${configEntry}
|
||||
@click=${this._openOptionsFlow}
|
||||
appearance="filled"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.configure"
|
||||
)}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>`;
|
||||
})}
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
${isComponentLoaded(this.hass, "hardware")
|
||||
? html`<ha-card outlined>
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.processor"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this._systemStatusData
|
||||
? html`${this._systemStatusData
|
||||
.cpu_percent}${blankBeforePercent(
|
||||
this.hass.locale
|
||||
)}%`
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content loading-container">
|
||||
<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._getChartData(this._cpuEntries)}
|
||||
.options=${this._chartOptions}
|
||||
></ha-chart-base>
|
||||
${!this._systemStatusData
|
||||
? html` <ha-fade-in delay="1000" class="loading-overlay">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-fade-in>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card outlined>
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.panel.config.hardware.memory")}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this._systemStatusData
|
||||
? html`${round(
|
||||
this._systemStatusData.memory_used_mb / 1024,
|
||||
1
|
||||
)}
|
||||
GB /
|
||||
${round(
|
||||
(this._systemStatusData.memory_used_mb +
|
||||
this._systemStatusData.memory_free_mb) /
|
||||
1024,
|
||||
0
|
||||
)}
|
||||
GB`
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content loading-container">
|
||||
<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._getChartData(this._memoryEntries)}
|
||||
.options=${this._chartOptions}
|
||||
></ha-chart-base>
|
||||
${!this._systemStatusData
|
||||
? html`
|
||||
<ha-fade-in delay="1000" class="loading-overlay">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _load() {
|
||||
if (isComponentLoaded(this.hass, "usb")) {
|
||||
await scanUSBDevices(this.hass);
|
||||
}
|
||||
|
||||
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
|
||||
try {
|
||||
if (isComponentLoaded(this.hass, "hardware")) {
|
||||
this._hardwareInfo = await this.hass.callWS({ type: "hardware/info" });
|
||||
}
|
||||
|
||||
if (isHassioLoaded && !this._hardwareInfo?.hardware.length) {
|
||||
this._OSData = await fetchHassioHassOsInfo(this.hass);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
private async _openOptionsFlow(ev) {
|
||||
const entry = ev.currentTarget.entry;
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
showOptionsFlowDialog(this, entry);
|
||||
}
|
||||
|
||||
private async _showRestartDialog() {
|
||||
showRestartDialog(this);
|
||||
}
|
||||
|
||||
private _getChartData = memoizeOne(
|
||||
(entries: [number, number | null][]): SeriesOption[] => [
|
||||
{
|
||||
...DATA_SET_CONFIG,
|
||||
id: entries === this._cpuEntries ? "cpu" : "memory",
|
||||
name:
|
||||
entries === this._cpuEntries
|
||||
? this.hass.localize("ui.panel.config.hardware.processor")
|
||||
: this.hass.localize("ui.panel.config.hardware.memory"),
|
||||
data: entries,
|
||||
} as SeriesOption,
|
||||
]
|
||||
);
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
--mdc-list-side-padding: 24px;
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.75);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.card-content img {
|
||||
max-width: 300px;
|
||||
margin: auto;
|
||||
}
|
||||
.board-info {
|
||||
text-align: center;
|
||||
}
|
||||
.primary-text {
|
||||
font-size: var(--ha-font-size-l);
|
||||
margin: 0;
|
||||
}
|
||||
.secondary-text {
|
||||
font-size: var(--ha-font-size-m);
|
||||
margin-bottom: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.header .value {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
--ha-alert-icon-size: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-hardware-overview": HaConfigHardwareOverview;
|
||||
}
|
||||
}
|
||||
@@ -1,585 +1,66 @@
|
||||
import { mdiPower } from "@mdi/js";
|
||||
import type { SeriesOption } from "echarts/types/dist/shared";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { mdiFormatListBulletedType, mdiMemory } from "@mdi/js";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { round } from "../../../common/number/round";
|
||||
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
|
||||
import "../../../components/chart/ha-chart-base";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fade-in";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { subscribeConfigEntries } from "../../../data/config_entries";
|
||||
import type {
|
||||
HardwareInfo,
|
||||
SystemStatusStreamMessage,
|
||||
} from "../../../data/hardware";
|
||||
import { BOARD_NAMES } from "../../../data/hardware";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type { HassioHassOSInfo } from "../../../data/hassio/host";
|
||||
import { fetchHassioHassOsInfo } from "../../../data/hassio/host";
|
||||
import { scanUSBDevices } from "../../../data/usb";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { ECOption } from "../../../resources/echarts/echarts";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { DefaultPrimaryColor } from "../../../resources/theme/color/color.globals";
|
||||
import type { RouterOptions } from "../../../layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../../layouts/hass-router-page";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { hardwareBrandsUrl } from "../../../util/brands-url";
|
||||
import { showhardwareAvailableDialog } from "./show-dialog-hardware-available";
|
||||
|
||||
const DATASAMPLES = 60;
|
||||
export const hardwareTabs = (hass: HomeAssistant): PageNavigation[] => {
|
||||
const tabs: PageNavigation[] = [
|
||||
{
|
||||
path: "/config/hardware/overview",
|
||||
translationKey: "ui.panel.config.hardware.overview",
|
||||
iconPath: mdiMemory,
|
||||
},
|
||||
];
|
||||
|
||||
const DATA_SET_CONFIG: SeriesOption = {
|
||||
type: "line",
|
||||
color: DefaultPrimaryColor,
|
||||
areaStyle: {
|
||||
color: DefaultPrimaryColor + "2B",
|
||||
},
|
||||
symbolSize: 0,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
smooth: 0.25,
|
||||
if (isComponentLoaded(hass, "hassio")) {
|
||||
tabs.push({
|
||||
path: "/config/hardware/all",
|
||||
translationKey: "ui.panel.config.hardware.system_hardware.title",
|
||||
iconPath: mdiFormatListBulletedType,
|
||||
});
|
||||
}
|
||||
|
||||
return tabs;
|
||||
};
|
||||
|
||||
@customElement("ha-config-hardware")
|
||||
class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
class HaConfigHardware extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "overview",
|
||||
routes: {
|
||||
overview: {
|
||||
tag: "ha-config-hardware-overview",
|
||||
load: () => import("./ha-config-hardware-overview"),
|
||||
cache: true,
|
||||
},
|
||||
all: {
|
||||
tag: "ha-config-hardware-all",
|
||||
load: () => import("./ha-config-hardware-all"),
|
||||
},
|
||||
},
|
||||
beforeRender: (page) => {
|
||||
if (
|
||||
page === "all" &&
|
||||
(!this.hass || !isComponentLoaded(this.hass, "hassio"))
|
||||
) {
|
||||
return "overview";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
|
||||
@state() private _OSData?: HassioHassOSInfo;
|
||||
|
||||
@state() private _hardwareInfo?: HardwareInfo;
|
||||
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
@state() private _systemStatusData?: SystemStatusStreamMessage;
|
||||
|
||||
@state() private _configEntries?: Record<string, ConfigEntry>;
|
||||
|
||||
private _memoryEntries: [number, number | null][] = [];
|
||||
|
||||
private _cpuEntries: [number, number | null][] = [];
|
||||
|
||||
public hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
const subs = [
|
||||
subscribeConfigEntries(
|
||||
this.hass,
|
||||
(messages) => {
|
||||
let fullUpdate = false;
|
||||
const newEntries: ConfigEntry[] = [];
|
||||
messages.forEach((message) => {
|
||||
if (message.type === null || message.type === "added") {
|
||||
newEntries.push(message.entry);
|
||||
if (message.type === null) {
|
||||
fullUpdate = true;
|
||||
}
|
||||
} else if (message.type === "removed") {
|
||||
if (this._configEntries) {
|
||||
delete this._configEntries[message.entry.entry_id];
|
||||
}
|
||||
} else if (message.type === "updated") {
|
||||
if (this._configEntries) {
|
||||
const newEntry = message.entry;
|
||||
this._configEntries[message.entry.entry_id] = newEntry;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!newEntries.length && !fullUpdate) {
|
||||
return;
|
||||
}
|
||||
const entries = [
|
||||
...(fullUpdate ? [] : Object.values(this._configEntries || {})),
|
||||
...newEntries,
|
||||
];
|
||||
const configEntries: Record<string, ConfigEntry> = {};
|
||||
for (const entry of entries) {
|
||||
configEntries[entry.entry_id] = entry;
|
||||
}
|
||||
this._configEntries = configEntries;
|
||||
},
|
||||
{ type: ["hardware"] }
|
||||
),
|
||||
];
|
||||
|
||||
if (isComponentLoaded(this.hass, "hardware")) {
|
||||
subs.push(
|
||||
this.hass.connection.subscribeMessage<SystemStatusStreamMessage>(
|
||||
(message) => {
|
||||
// Only store the last 60 entries
|
||||
this._memoryEntries.shift();
|
||||
this._cpuEntries.shift();
|
||||
|
||||
this._memoryEntries.push([
|
||||
new Date(message.timestamp).getTime(),
|
||||
message.memory_used_percent,
|
||||
]);
|
||||
this._cpuEntries.push([
|
||||
new Date(message.timestamp).getTime(),
|
||||
message.cpu_percent,
|
||||
]);
|
||||
|
||||
this._systemStatusData = message;
|
||||
},
|
||||
{
|
||||
type: "hardware/subscribe_system_status",
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return subs;
|
||||
protected updatePageEl(pageEl) {
|
||||
pageEl.hass = this.hass;
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.route = this.routeTail;
|
||||
}
|
||||
|
||||
protected willUpdate(): void {
|
||||
if (!this.hasUpdated && !this._chartOptions) {
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitLine: {
|
||||
show: true,
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: (value: number) =>
|
||||
value + blankBeforePercent(this.hass.locale) + "%",
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
scale: true,
|
||||
},
|
||||
grid: {
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
right: 10,
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
valueFormatter: (value) =>
|
||||
value + blankBeforePercent(this.hass.locale) + "%",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._load();
|
||||
|
||||
const date = new Date();
|
||||
// Force graph to start drawing from the right
|
||||
for (let i = 0; i < DATASAMPLES; i++) {
|
||||
const t = new Date(date);
|
||||
t.setSeconds(t.getSeconds() - 5 * (DATASAMPLES - i));
|
||||
this._memoryEntries.push([t.getTime(), null]);
|
||||
this._cpuEntries.push([t.getTime(), null]);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
let boardId: string | undefined;
|
||||
let boardName: string | undefined;
|
||||
let imageURL: string | undefined;
|
||||
let documentationURL: string | undefined;
|
||||
let boardConfigEntries: ConfigEntry[] = [];
|
||||
|
||||
const boardData = this._hardwareInfo?.hardware.find(
|
||||
(hw) => hw.board !== null
|
||||
);
|
||||
|
||||
const dongles = this._hardwareInfo?.hardware.filter(
|
||||
(hw) =>
|
||||
hw.dongle !== null &&
|
||||
(!hw.config_entries.length ||
|
||||
hw.config_entries.some(
|
||||
(entryId) =>
|
||||
this._configEntries?.[entryId] &&
|
||||
!this._configEntries[entryId].disabled_by
|
||||
))
|
||||
);
|
||||
|
||||
if (boardData) {
|
||||
boardConfigEntries = boardData.config_entries
|
||||
.map((id) => this._configEntries?.[id])
|
||||
.filter(
|
||||
(entry) => entry?.supports_options && !entry.disabled_by
|
||||
) as ConfigEntry[];
|
||||
boardId = boardData.board!.hassio_board_id;
|
||||
boardName = boardData.name;
|
||||
documentationURL = boardData.url;
|
||||
imageURL = hardwareBrandsUrl(
|
||||
{
|
||||
category: "boards",
|
||||
manufacturer: boardData.board!.manufacturer,
|
||||
model: boardData.board!.model,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
);
|
||||
} else if (this._OSData?.board) {
|
||||
boardId = this._OSData.board;
|
||||
boardName = BOARD_NAMES[this._OSData.board];
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.hardware.caption")}
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio")
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiPower}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.hardware.restart_homeassistant"
|
||||
)}
|
||||
@click=${this._showRestartDialog}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<div class="content">
|
||||
${boardName || isComponentLoaded(this.hass, "hassio")
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${imageURL
|
||||
? html`<img
|
||||
alt=""
|
||||
src=${imageURL}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>`
|
||||
: nothing}
|
||||
<div class="board-info">
|
||||
<p class="primary-text">
|
||||
${boardName ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.hardware.generic_hardware"
|
||||
)}
|
||||
</p>
|
||||
${boardId
|
||||
? html`<p class="secondary-text">${boardId}</p>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
${documentationURL
|
||||
? html`
|
||||
<ha-md-list-item
|
||||
.href=${documentationURL}
|
||||
type="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.hardware.documentation"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.hardware.documentation_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${boardConfigEntries.length ||
|
||||
isComponentLoaded(this.hass, "hassio")
|
||||
? html`<div class="card-actions">
|
||||
${boardConfigEntries.length
|
||||
? html`
|
||||
<ha-button
|
||||
.entry=${boardConfigEntries[0]}
|
||||
@click=${this._openOptionsFlow}
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.configure"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
${isComponentLoaded(this.hass, "hassio")
|
||||
? html`
|
||||
<ha-button
|
||||
@click=${this._openHardware}
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.title"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
${dongles?.length
|
||||
? html`<ha-card outlined>
|
||||
${dongles.map((dongle) => {
|
||||
const configEntry = dongle.config_entries
|
||||
.map((id) => this._configEntries?.[id])
|
||||
.filter(
|
||||
(entry) => entry?.supports_options && !entry.disabled_by
|
||||
)[0];
|
||||
return html`<div class="row">
|
||||
${dongle.name}${configEntry
|
||||
? html`<ha-button
|
||||
.entry=${configEntry}
|
||||
@click=${this._openOptionsFlow}
|
||||
appearance="filled"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.configure"
|
||||
)}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>`;
|
||||
})}
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
${isComponentLoaded(this.hass, "hardware")
|
||||
? html`<ha-card outlined>
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.processor"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this._systemStatusData
|
||||
? html`${this._systemStatusData
|
||||
.cpu_percent}${blankBeforePercent(
|
||||
this.hass.locale
|
||||
)}%`
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content loading-container">
|
||||
<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._getChartData(this._cpuEntries)}
|
||||
.options=${this._chartOptions}
|
||||
></ha-chart-base>
|
||||
${!this._systemStatusData
|
||||
? html` <ha-fade-in delay="1000" class="loading-overlay">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-fade-in>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card outlined>
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.panel.config.hardware.memory")}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this._systemStatusData
|
||||
? html`${round(
|
||||
this._systemStatusData.memory_used_mb / 1024,
|
||||
1
|
||||
)}
|
||||
GB /
|
||||
${round(
|
||||
(this._systemStatusData.memory_used_mb +
|
||||
this._systemStatusData.memory_free_mb) /
|
||||
1024,
|
||||
0
|
||||
)}
|
||||
GB`
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content loading-container">
|
||||
<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._getChartData(this._memoryEntries)}
|
||||
.options=${this._chartOptions}
|
||||
></ha-chart-base>
|
||||
${!this._systemStatusData
|
||||
? html`
|
||||
<ha-fade-in delay="1000" class="loading-overlay">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _load() {
|
||||
if (isComponentLoaded(this.hass, "usb")) {
|
||||
await scanUSBDevices(this.hass);
|
||||
}
|
||||
|
||||
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
|
||||
try {
|
||||
if (isComponentLoaded(this.hass, "hardware")) {
|
||||
this._hardwareInfo = await this.hass.callWS({ type: "hardware/info" });
|
||||
}
|
||||
|
||||
if (isHassioLoaded && !this._hardwareInfo?.hardware.length) {
|
||||
this._OSData = await fetchHassioHassOsInfo(this.hass);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
private async _openOptionsFlow(ev) {
|
||||
const entry = ev.currentTarget.entry;
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
showOptionsFlowDialog(this, entry);
|
||||
}
|
||||
|
||||
private async _openHardware() {
|
||||
showhardwareAvailableDialog(this);
|
||||
}
|
||||
|
||||
private async _showRestartDialog() {
|
||||
showRestartDialog(this);
|
||||
}
|
||||
|
||||
private _getChartData = memoizeOne(
|
||||
(entries: [number, number | null][]): SeriesOption[] => [
|
||||
{
|
||||
...DATA_SET_CONFIG,
|
||||
id: entries === this._cpuEntries ? "cpu" : "memory",
|
||||
name:
|
||||
entries === this._cpuEntries
|
||||
? this.hass.localize("ui.panel.config.hardware.processor")
|
||||
: this.hass.localize("ui.panel.config.hardware.memory"),
|
||||
data: entries,
|
||||
} as SeriesOption,
|
||||
]
|
||||
);
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
--mdc-list-side-padding: 24px;
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.75);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.card-content img {
|
||||
max-width: 300px;
|
||||
margin: auto;
|
||||
}
|
||||
.board-info {
|
||||
text-align: center;
|
||||
}
|
||||
.primary-text {
|
||||
font-size: var(--ha-font-size-l);
|
||||
margin: 0;
|
||||
}
|
||||
.secondary-text {
|
||||
font-size: var(--ha-font-size-m);
|
||||
margin-bottom: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.header .value {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
--ha-alert-icon-size: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export const loadHardwareAvailableDialog = () =>
|
||||
import("./dialog-hardware-available");
|
||||
|
||||
export const showhardwareAvailableDialog = (element: HTMLElement): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-dialog-hardware-available",
|
||||
dialogImport: loadHardwareAvailableDialog,
|
||||
dialogParams: {},
|
||||
});
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeEntityEntryName } from "../../../../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../../../../common/entity/compute_state_name";
|
||||
import { stringCompare } from "../../../../../common/string/compare";
|
||||
import "../../../../../components/entity/state-badge";
|
||||
import "../../../../../components/ha-area-picker";
|
||||
@@ -12,13 +12,17 @@ import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import { updateDeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../../../data/entity/entity_registry";
|
||||
import { subscribeEntityRegistry } from "../../../../../data/entity/entity_registry";
|
||||
import {
|
||||
getAutomaticEntityIds,
|
||||
subscribeEntityRegistry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../../../data/entity/entity_registry";
|
||||
import type { ZHADevice } from "../../../../../data/zha";
|
||||
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { EntityRegistryEntryWithDisplayName } from "../../../devices/ha-config-device-page";
|
||||
import type { EntityRegistryStateEntry } from "../../../devices/ha-config-device-page";
|
||||
|
||||
@customElement("zha-device-card")
|
||||
class ZHADeviceCard extends SubscribeMixin(LitElement) {
|
||||
@@ -34,17 +38,17 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
|
||||
(
|
||||
deviceId: string,
|
||||
entities: EntityRegistryEntry[]
|
||||
): EntityRegistryEntryWithDisplayName[] =>
|
||||
): EntityRegistryStateEntry[] =>
|
||||
entities
|
||||
.filter((entity) => entity.device_id === deviceId)
|
||||
.map((entity) => ({
|
||||
...entity,
|
||||
display_name: computeEntityEntryName(entity),
|
||||
stateName: this._computeEntityName(entity),
|
||||
}))
|
||||
.sort((ent1, ent2) =>
|
||||
stringCompare(
|
||||
ent1.display_name || "",
|
||||
ent2.display_name || "",
|
||||
ent1.stateName || `zzz${ent1.entity_id}`,
|
||||
ent2.stateName || `zzz${ent2.entity_id}`,
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
@@ -85,7 +89,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
|
||||
? html`
|
||||
<state-badge
|
||||
@click=${this._openMoreInfo}
|
||||
.title=${entity.display_name || ""}
|
||||
.title=${entity.stateName!}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass!.states[entity.entity_id]}
|
||||
slot="item-icon"
|
||||
@@ -116,11 +120,52 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
|
||||
if (!this.hass || !this.device) {
|
||||
return;
|
||||
}
|
||||
const device = this.device;
|
||||
|
||||
const oldDeviceName = device.user_given_name || device.name;
|
||||
const newDeviceName = event.target.value;
|
||||
this.device.user_given_name = newDeviceName;
|
||||
await updateDeviceRegistryEntry(this.hass, this.device.device_reg_id, {
|
||||
await updateDeviceRegistryEntry(this.hass, device.device_reg_id, {
|
||||
name_by_user: newDeviceName,
|
||||
});
|
||||
|
||||
if (!oldDeviceName || !newDeviceName || oldDeviceName === newDeviceName) {
|
||||
return;
|
||||
}
|
||||
const entities = this._deviceEntities(device.device_reg_id, this._entities);
|
||||
|
||||
const entityIdsMapping = await getAutomaticEntityIds(
|
||||
this.hass,
|
||||
entities.map((entity) => entity.entity_id)
|
||||
);
|
||||
|
||||
const updateProms = entities.map((entity) => {
|
||||
const name = entity.name;
|
||||
const newEntityId = entityIdsMapping[entity.entity_id];
|
||||
let newName: string | null | undefined;
|
||||
|
||||
if (entity.has_entity_name && !entity.name) {
|
||||
newName = undefined;
|
||||
} else if (
|
||||
entity.has_entity_name &&
|
||||
(entity.name === oldDeviceName || entity.name === newDeviceName)
|
||||
) {
|
||||
// clear name if it matches the device name and it uses the device name (entity naming)
|
||||
newName = null;
|
||||
} else if (name && name.includes(oldDeviceName)) {
|
||||
newName = name.replace(oldDeviceName, newDeviceName);
|
||||
}
|
||||
|
||||
if (newName !== undefined && !newEntityId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
|
||||
name: newName,
|
||||
new_entity_id: newEntityId || undefined,
|
||||
});
|
||||
});
|
||||
await Promise.all(updateProms);
|
||||
}
|
||||
|
||||
private _openMoreInfo(ev: MouseEvent): void {
|
||||
@@ -129,6 +174,13 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _computeEntityName(entity: EntityRegistryEntry): string | null {
|
||||
if (this.hass.states[entity.entity_id]) {
|
||||
return computeStateName(this.hass.states[entity.entity_id]);
|
||||
}
|
||||
return entity.name;
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: CustomEvent) {
|
||||
const picker = ev.currentTarget as any;
|
||||
|
||||
|
||||
+25
-2
@@ -890,6 +890,8 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._step = "rename_device";
|
||||
const nameChanged = this._device.name !== this._deviceOptions?.name;
|
||||
if (nameChanged || this._deviceOptions?.area) {
|
||||
const oldDeviceName = this._device.name;
|
||||
const newDeviceName = this._deviceOptions!.name;
|
||||
try {
|
||||
await updateDeviceRegistryEntry(this.hass, this._device.id, {
|
||||
name_by_user: this._deviceOptions!.name,
|
||||
@@ -897,6 +899,8 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
});
|
||||
|
||||
if (nameChanged) {
|
||||
// rename entities
|
||||
|
||||
const entities = this._entities.filter(
|
||||
(entity) => entity.device_id === this._device!.id
|
||||
);
|
||||
@@ -908,14 +912,33 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
|
||||
await Promise.all(
|
||||
entities.map((entity) => {
|
||||
const name = entity.name;
|
||||
let newName: string | null | undefined;
|
||||
const newEntityId = entityIdsMapping[entity.entity_id];
|
||||
|
||||
if (!newEntityId || newEntityId === entity.entity_id) {
|
||||
if (entity.has_entity_name && !entity.name) {
|
||||
newName = undefined;
|
||||
} else if (
|
||||
entity.has_entity_name &&
|
||||
(entity.name === oldDeviceName ||
|
||||
entity.name === newDeviceName)
|
||||
) {
|
||||
// clear name if it matches the device name and it uses the device name (entity naming)
|
||||
newName = null;
|
||||
} else if (name && name.includes(oldDeviceName)) {
|
||||
newName = name.replace(oldDeviceName, newDeviceName);
|
||||
}
|
||||
|
||||
if (
|
||||
(newName === undefined && !newEntityId) ||
|
||||
newEntityId === entity.entity_id
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
|
||||
new_entity_id: newEntityId,
|
||||
name: newName || name,
|
||||
new_entity_id: newEntityId || undefined,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -4,11 +4,11 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-dialog";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
@@ -79,7 +79,7 @@ class MoveDatadiskDialog extends LitElement {
|
||||
this.closeDialog();
|
||||
await showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.hardware.available_hardware.failed_to_get"
|
||||
"ui.panel.config.hardware.system_hardware.failed_to_get"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
|
||||
@@ -19,7 +19,6 @@ import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
@@ -175,11 +174,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
"--badge-color": color,
|
||||
};
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
const stateDisplay = html`
|
||||
<state-display
|
||||
|
||||
@@ -542,6 +542,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
this._legendData = chartData.map((d) => ({
|
||||
...d,
|
||||
name: this._getDeviceName(d.name),
|
||||
value: `${formatNumber(d.value[0], this.hass.locale)} kWh`,
|
||||
}));
|
||||
// filter out hidden stats in place
|
||||
for (let i = chartData.length - 1; i >= 0; i--) {
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type { LovelaceCard } from "../types";
|
||||
@@ -233,11 +232,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const defaultCode = this._entry?.options?.alarm_control_panel?.default_code;
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
|
||||
@@ -41,7 +41,6 @@ import type { FrontendLocaleData } from "../../../data/translation";
|
||||
import type { Themes } from "../../../data/ws-themes";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -52,6 +51,21 @@ import type {
|
||||
} from "../types";
|
||||
import type { ButtonCardConfig } from "./types";
|
||||
|
||||
const EMPTY_STATE_OBJ = {
|
||||
state: "unavailable",
|
||||
attributes: {
|
||||
friendly_name: "",
|
||||
},
|
||||
entity_id: "___.empty",
|
||||
context: {
|
||||
id: "",
|
||||
parent_id: null,
|
||||
user_id: null,
|
||||
},
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
} satisfies HassEntity;
|
||||
|
||||
export const getEntityDefaultButtonAction = (entityId?: string) =>
|
||||
entityId && DOMAINS_TOGGLE.has(computeDomain(entityId))
|
||||
? "toggle"
|
||||
@@ -183,9 +197,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
const name = this.hass.formatEntityName(
|
||||
stateObj || EMPTY_STATE_OBJ,
|
||||
this._config.name
|
||||
);
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { normalizeValueBySIPrefix } from "../../../common/number/normalize-by-si-prefix";
|
||||
import { MobileAwareMixin } from "../../../mixins/mobile-aware-mixin";
|
||||
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-segmented-bar";
|
||||
@@ -240,7 +239,7 @@ export class HuiDistributionCard
|
||||
const color = entity.color
|
||||
? computeCssColor(entity.color)
|
||||
: getGraphColorByIndex(entity.originalIndex, computedStyles);
|
||||
const name = computeLovelaceEntityName(this.hass!, stateObj, entity.name);
|
||||
const name = this.hass!.formatEntityName(stateObj, entity.name);
|
||||
const formattedValue = this.hass!.formatEntityState(stateObj);
|
||||
|
||||
segments.push({
|
||||
@@ -276,7 +275,7 @@ export class HuiDistributionCard
|
||||
const isZeroOrNegative = !stateObj || value <= 0 || isNaN(value);
|
||||
|
||||
const name = stateObj
|
||||
? computeLovelaceEntityName(this.hass!, stateObj, entity.name)
|
||||
? this.hass!.formatEntityName(stateObj, entity.name)
|
||||
: entity.entity;
|
||||
|
||||
const formattedValue = stateObj
|
||||
|
||||
@@ -25,7 +25,6 @@ import { handleAction } from "../common/handle-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -143,14 +142,13 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
const indexUnit = stateParts.findIndex((part) => part.type === "unit");
|
||||
const indexValue = stateParts.findIndex((part) => part.type === "value");
|
||||
const indexValue = stateParts.reduceRight(
|
||||
(acc, part, i) => (acc === -1 && part.type === "value" ? i : acc),
|
||||
-1
|
||||
);
|
||||
const reversedOrder = indexUnit !== -1 && indexUnit < indexValue;
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
const colored = stateObj && this._getStateColor(stateObj, this._config);
|
||||
|
||||
@@ -209,7 +207,10 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
>
|
||||
</ha-attribute-value>`
|
||||
: this.hass.localize("state.default.unknown")
|
||||
: stateParts.find((part) => part.type === "value")?.value}</span
|
||||
: stateParts
|
||||
.filter((part) => part.type === "value")
|
||||
.map((part) => part.value)
|
||||
.join("")}</span
|
||||
>${unit
|
||||
? html`<span
|
||||
class=${classMap({
|
||||
|
||||
@@ -14,7 +14,6 @@ import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
@@ -124,11 +123,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
// Use `stateObj.state` as value to keep formatting (e.g trailing zeros)
|
||||
// for consistent value display across gauge, entity, entity-row, etc.
|
||||
|
||||
@@ -18,7 +18,6 @@ import type {
|
||||
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
@@ -252,11 +251,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass!,
|
||||
stateObj,
|
||||
entityConf.name
|
||||
);
|
||||
const name = this.hass!.formatEntityName(stateObj, entityConf.name);
|
||||
|
||||
return html`
|
||||
<div
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import { fetchStatistics } from "../../../data/recorder";
|
||||
import { getSensorNumericDeviceClasses } from "../../../data/sensor";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import type { EntityConfig } from "../entity-rows/types";
|
||||
@@ -106,7 +105,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
this._entities.forEach((entity) => {
|
||||
const stateObj = this.hass!.states[entity.entity];
|
||||
this._names[entity.entity] = stateObj
|
||||
? computeLovelaceEntityName(this.hass!, stateObj, entity.name)
|
||||
? this.hass!.formatEntityName(stateObj, entity.name)
|
||||
: entity.entity;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import "../../../state-control/humidifier/ha-state-control-humidifier-humidity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../card-features/hui-card-features";
|
||||
import type { LovelaceCardFeatureContext } from "../card-features/types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type {
|
||||
@@ -133,11 +132,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
const color = stateColorCss(stateObj);
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import { lightSupportsBrightness } from "../../../data/light";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
@@ -92,11 +91,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
((stateObj.attributes.brightness || 0) / 255) * 100
|
||||
);
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
|
||||
@@ -424,8 +424,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
const computedStyles = getComputedStyle(this);
|
||||
color = getColorByIndex(this._colorIndex, computedStyles);
|
||||
this._colorIndex++;
|
||||
this._colorDict[entityId] = color;
|
||||
if (color) {
|
||||
this._colorIndex++;
|
||||
this._colorDict[entityId] = color;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
mediaPlayerPlayMedia,
|
||||
} from "../../../data/media-player";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-marquee";
|
||||
@@ -242,8 +241,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
<div>
|
||||
${computeLovelaceEntityName(
|
||||
this.hass,
|
||||
${this.hass.formatEntityName(
|
||||
this.hass!.states[this._config!.entity],
|
||||
this._config.name
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,6 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { PersonEntity } from "../../../data/person";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
@@ -126,11 +125,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
const entityState = this.hass.formatEntityState(stateObj);
|
||||
|
||||
let footer: TemplateResult | string = "";
|
||||
|
||||
@@ -15,7 +15,6 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -119,7 +118,7 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard {
|
||||
style="background-image:url(${stateObj.attributes.entity_picture})"
|
||||
>
|
||||
<div class="header">
|
||||
${computeLovelaceEntityName(this.hass, stateObj, this._config.name)}
|
||||
${this.hass.formatEntityName(stateObj, this._config.name)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
@@ -2,18 +2,13 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { haStyleScrollbar } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import { createErrorCardElement } from "../create-element/create-element-base";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import "./hui-card";
|
||||
import type { HuiCard } from "./hui-card";
|
||||
import type { StackCardConfig } from "./types";
|
||||
import { createErrorCardElement } from "../create-element/create-element-base";
|
||||
import type { HuiErrorCard } from "./hui-error-card";
|
||||
import type { StackCardConfig } from "./types";
|
||||
|
||||
export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
extends LitElement
|
||||
@@ -44,14 +39,6 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
return 1;
|
||||
}
|
||||
|
||||
public getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 12,
|
||||
rows: "auto",
|
||||
min_columns: 3,
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: T): void {
|
||||
if (!config || !config.cards || !Array.isArray(config.cards)) {
|
||||
throw new Error("Invalid configuration");
|
||||
@@ -117,11 +104,7 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
${this._config.title
|
||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
||||
: ""}
|
||||
<div
|
||||
id="root"
|
||||
class="ha-scrollbar"
|
||||
dir=${this.hass ? computeRTLDirection(this.hass) : "ltr"}
|
||||
>
|
||||
<div id="root" dir=${this.hass ? computeRTLDirection(this.hass) : "ltr"}>
|
||||
${this._cards}
|
||||
${this.preview && this._errorCard ? this._errorCard : nothing}
|
||||
</div>
|
||||
@@ -129,7 +112,6 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
}
|
||||
|
||||
static sharedStyles = [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
.card-header {
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
} from "../../../data/recorder";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
|
||||
@@ -200,7 +199,7 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
const name =
|
||||
(this._config.name
|
||||
? computeLovelaceEntityName(this.hass, stateObj, this._config.name)
|
||||
? this.hass.formatEntityName(stateObj, this._config.name)
|
||||
: "") ||
|
||||
getStatisticLabel(this.hass, this._config.entity, this._metadata);
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
getStatisticMetadata,
|
||||
} from "../../../data/recorder";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
@@ -189,8 +188,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
this._entities.forEach((config) => {
|
||||
const stateObj = this.hass!.states[config.entity];
|
||||
this._names[config.entity] =
|
||||
computeLovelaceEntityName(this.hass!, stateObj, config.name) ||
|
||||
config.entity;
|
||||
this.hass!.formatEntityName(stateObj, config.name) || config.entity;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import "../../../state-control/water_heater/ha-state-control-water_heater-temper
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../card-features/hui-card-features";
|
||||
import type { LovelaceCardFeatureContext } from "../card-features/types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type {
|
||||
@@ -132,11 +131,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
const color = stateColorCss(stateObj);
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import "../../../state-display/state-display";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../card-features/hui-card-features";
|
||||
import type { LovelaceCardFeatureContext } from "../card-features/types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
@@ -252,11 +251,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
const active = stateActive(stateObj);
|
||||
const color = this._computeStateColor(stateObj, this._config.color);
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
} from "../../../data/weather";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
@@ -231,7 +230,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
return html`
|
||||
<ha-card class="unavailable" @click=${this._handleAction}>
|
||||
${this.hass.localize("ui.panel.lovelace.warning.entity_unavailable", {
|
||||
entity: `${computeLovelaceEntityName(this.hass, stateObj, this._config.name)} (${this._config.entity})`,
|
||||
entity: `${this.hass.formatEntityName(stateObj, this._config.name)} (${this._config.entity})`,
|
||||
})}
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -262,11 +261,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
const dayNight = forecastData?.type === "twice_daily";
|
||||
|
||||
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
const temperatureFractionDigits = this._config.round_temperature
|
||||
? 0
|
||||
|
||||
@@ -155,7 +155,7 @@ export interface AreaCardConfig extends LovelaceCardConfig {
|
||||
|
||||
export interface ButtonCardConfig extends LovelaceCardConfig {
|
||||
entity?: string;
|
||||
name?: string;
|
||||
name?: string | EntityNameItem | EntityNameItem[];
|
||||
show_name?: boolean;
|
||||
icon?: string;
|
||||
icon_height?: string;
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import {
|
||||
DEFAULT_ENTITY_NAME,
|
||||
type EntityNameItem,
|
||||
} from "../../../../common/entity/compute_entity_name_display";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
/**
|
||||
* Computes the display name for an entity in Lovelace (cards and badges).
|
||||
*
|
||||
* @param hass - The Home Assistant instance
|
||||
* @param stateObj - The entity state object
|
||||
* @param config - The name configuration (string for override, or EntityNameItem[] for structured naming)
|
||||
* @returns The computed entity name
|
||||
*/
|
||||
export const computeLovelaceEntityName = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: HassEntity | undefined,
|
||||
config: string | EntityNameItem | EntityNameItem[] | undefined
|
||||
): string => {
|
||||
if (typeof config === "string") {
|
||||
return config;
|
||||
}
|
||||
if (stateObj) {
|
||||
return hass.formatEntityName(stateObj, config ?? DEFAULT_ENTITY_NAME);
|
||||
}
|
||||
if (!config) {
|
||||
return "";
|
||||
}
|
||||
// If entity is not found, fall back to text parts in config
|
||||
// This allows for static names even when the entity is missing
|
||||
// e.g. for a card that doesn't require an entity
|
||||
const textParts = ensureArray(config)
|
||||
.filter((item) => item.type === "text")
|
||||
.map((item) => ("text" in item ? item.text : ""));
|
||||
if (textParts.length) {
|
||||
return textParts.join(" ");
|
||||
}
|
||||
return "";
|
||||
};
|
||||
@@ -32,7 +32,6 @@ import type {
|
||||
} from "../cards/types";
|
||||
import type { EntityConfig } from "../entity-rows/types";
|
||||
import type { ButtonsHeaderFooterConfig } from "../header-footer/types";
|
||||
import { computeLovelaceEntityName } from "./entity/compute-lovelace-entity-name";
|
||||
|
||||
const HIDE_DOMAIN = new Set([
|
||||
"ai_task",
|
||||
@@ -271,14 +270,14 @@ export const computeCards = (
|
||||
? computeStateName(states[a])
|
||||
: ""
|
||||
: states[a.entity]
|
||||
? computeLovelaceEntityName(hass, states[a.entity], a.name)
|
||||
? hass.formatEntityName(states[a.entity], a.name)
|
||||
: "",
|
||||
typeof b === "string"
|
||||
? states[b]
|
||||
? computeStateName(states[b])
|
||||
: ""
|
||||
: states[b.entity]
|
||||
? computeLovelaceEntityName(hass, states[b.entity], b.name)
|
||||
? hass.formatEntityName(states[b.entity], b.name)
|
||||
: ""
|
||||
);
|
||||
});
|
||||
|
||||
@@ -42,7 +42,8 @@ export class HuiEntityEditor extends LitElement {
|
||||
const stateObj = this.hass.states[item.entity];
|
||||
|
||||
const useDeviceName =
|
||||
stateObj && entityUseDeviceName(stateObj, this.hass.entities);
|
||||
stateObj &&
|
||||
entityUseDeviceName(stateObj, this.hass.entities, this.hass.devices);
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
import { createEntityNotFoundWarning } from "./hui-warning";
|
||||
@@ -66,11 +65,7 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
const pointer = hasAnyAction(this.config);
|
||||
|
||||
const hasSecondary = this.secondaryText || this.config.secondary_info;
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this.config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this.config.name);
|
||||
|
||||
return html`
|
||||
<div
|
||||
|
||||
@@ -197,10 +197,10 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
disabled: this._shouldDisableOptions(entityId),
|
||||
selector: {
|
||||
attribute: {
|
||||
entity_id: entityId,
|
||||
hide_attributes: ["entity_picture", "friendly_name", "icon"],
|
||||
},
|
||||
},
|
||||
context: { filter_entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "unit",
|
||||
|
||||
@@ -293,6 +293,7 @@ export class HuiTileCardEditor
|
||||
|
||||
const featureContext = this._featureContext(entityId);
|
||||
const hasCompatibleFeatures = this._hasCompatibleFeatures(featureContext);
|
||||
const hasFeatures = (this._config.features?.length ?? 0) > 0;
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -311,7 +312,14 @@ export class HuiTileCardEditor
|
||||
)}
|
||||
</h3>
|
||||
<div class="content">
|
||||
${hasCompatibleFeatures
|
||||
<hui-card-features-editor
|
||||
.hass=${this.hass}
|
||||
.context=${featureContext}
|
||||
.features=${this._config!.features ?? []}
|
||||
@features-changed=${this._featuresChanged}
|
||||
@edit-detail-element=${this._editDetailElement}
|
||||
></hui-card-features-editor>
|
||||
${hasCompatibleFeatures && hasFeatures
|
||||
? html`
|
||||
<ha-form
|
||||
class="features-form"
|
||||
@@ -324,13 +332,6 @@ export class HuiTileCardEditor
|
||||
></ha-form>
|
||||
`
|
||||
: nothing}
|
||||
<hui-card-features-editor
|
||||
.hass=${this.hass}
|
||||
.context=${featureContext}
|
||||
.features=${this._config!.features ?? []}
|
||||
@features-changed=${this._featuresChanged}
|
||||
@edit-detail-element=${this._editDetailElement}
|
||||
></hui-card-features-editor>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
@@ -478,7 +479,8 @@ export class HuiTileCardEditor
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.features-form {
|
||||
margin-bottom: 8px;
|
||||
margin-top: var(--ha-space-6);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -7,7 +7,6 @@ import "../../../components/ha-time-input";
|
||||
import { setDateTimeValue } from "../../../data/datetime";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -53,11 +52,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
|
||||
const time = dateObj ? format(dateObj, "HH:mm:ss") : undefined;
|
||||
const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined;
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass!,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass!.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
stateToIsoDateString,
|
||||
} from "../../../data/input_datetime";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -47,11 +46,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass!,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass!.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { InputSelectEntity } from "../../../data/input_select";
|
||||
import { setInputSelectOption } from "../../../data/input_select";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -50,11 +49,7 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass!,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass!.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
|
||||
@@ -5,7 +5,6 @@ import "../../../components/ha-textfield";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import { setValue } from "../../../data/input_text";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -43,11 +42,7 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass!,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass!.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { SelectEntity } from "../../../data/select";
|
||||
import { setSelectOption } from "../../../data/select";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -68,11 +67,7 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass!,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass!.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
|
||||
@@ -6,7 +6,6 @@ import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { TextEntity } from "../../../data/text";
|
||||
import { setValue } from "../../../data/text";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -46,11 +45,7 @@ class HuiTextEntityRow extends LitElement implements LovelaceRow {
|
||||
`;
|
||||
}
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass!,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass!.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
@@ -119,11 +118,7 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
const forecastData = getForecast(stateObj.attributes, this._forecastEvent);
|
||||
const forecast = forecastData?.forecast;
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass!,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass!.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<div
|
||||
|
||||
@@ -15,7 +15,6 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import "../../../state-display/state-display";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
@@ -154,11 +153,7 @@ export class HuiEntityHeadingBadge
|
||||
"--icon-color": color,
|
||||
};
|
||||
|
||||
const name = computeLovelaceEntityName(
|
||||
this.hass,
|
||||
stateObj,
|
||||
this._config.name
|
||||
);
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<ha-heading-badge
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user