mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-30 01:24:19 +00:00
Compare commits
218
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f11dc4580 | ||
|
|
27489f188e | ||
|
|
97fffc3b43 | ||
|
|
a30193b8ef | ||
|
|
cf063dc19f | ||
|
|
5db829b7be | ||
|
|
421f8b1986 | ||
|
|
a140eedc6e | ||
|
|
2933568658 | ||
|
|
29faeaf7fa | ||
|
|
95ba532ceb | ||
|
|
352ecb9d6d | ||
|
|
3f73049a3d | ||
|
|
50ad0c6cd0 | ||
|
|
78f95bbb06 | ||
|
|
6134056776 | ||
|
|
b79c6350ec | ||
|
|
8148041ddc | ||
|
|
4265624c0e | ||
|
|
ed4179de8f | ||
|
|
e407a1b2dd | ||
|
|
e851101a93 | ||
|
|
d43e1ef4cc | ||
|
|
f699019994 | ||
|
|
fe90b0339d | ||
|
|
3b5ba802f6 | ||
|
|
b0c6e7f564 | ||
|
|
658a23724e | ||
|
|
f859183628 | ||
|
|
53dc14f413 | ||
|
|
f2523095ff | ||
|
|
ef5c04b67f | ||
|
|
10ed6f709e | ||
|
|
c22de3bc7c | ||
|
|
37f67a2f45 | ||
|
|
06df9e3801 | ||
|
|
7f45b38d6f | ||
|
|
5c0f9b6501 | ||
|
|
645811dfdf | ||
|
|
45ec2e805a | ||
|
|
18c93baa70 | ||
|
|
2518c6ea77 | ||
|
|
702381515a | ||
|
|
93efd84fab | ||
|
|
be3841ff0d | ||
|
|
bfd6ff56a5 | ||
|
|
41ce82ca01 | ||
|
|
f8826fc821 | ||
|
|
c0a98dba54 | ||
|
|
839a910a19 | ||
|
|
345745ef47 | ||
|
|
c4c0ae0dde | ||
|
|
6297d3e972 | ||
|
|
8985b7d08d | ||
|
|
9f6de3166f | ||
|
|
64c6f2a133 | ||
|
|
d1c1d9a05d | ||
|
|
ef531bcb84 | ||
|
|
14252d09fb | ||
|
|
12e32ce0ce | ||
|
|
5c88dc99c2 | ||
|
|
47c3b2f57c | ||
|
|
d1c2915c4e | ||
|
|
7f1aedcb95 | ||
|
|
db63231a5d | ||
|
|
0aabfa89bc | ||
|
|
35601830a8 | ||
|
|
fb882dacae | ||
|
|
aaad3f0d3d | ||
|
|
ce2a9577e5 | ||
|
|
5bafeb875c | ||
|
|
a24881a54f | ||
|
|
4decd87fd6 | ||
|
|
7fd2a112bf | ||
|
|
fc22a2af50 | ||
|
|
bcae4feba9 | ||
|
|
d0c81d6d5f | ||
|
|
711ff63983 | ||
|
|
1bc800d042 | ||
|
|
96605b8986 | ||
|
|
7d374f9798 | ||
|
|
920cca10d1 | ||
|
|
cc3334dfce | ||
|
|
fcd17e3f2a | ||
|
|
da0935c904 | ||
|
|
38bf0a9ac3 | ||
|
|
01bf2a402b | ||
|
|
4b20e52c29 | ||
|
|
cdc08029e0 | ||
|
|
5bbe135c53 | ||
|
|
68445a8f94 | ||
|
|
316043960c | ||
|
|
d8dde4a0bb | ||
|
|
525f2c23d6 | ||
|
|
41d43fe780 | ||
|
|
5b4a9a8967 | ||
|
|
3fe979408a | ||
|
|
8b0d03cad2 | ||
|
|
76ee92807a | ||
|
|
9f4fd39f3f | ||
|
|
1fe09e6dbd | ||
|
|
f74c63f257 | ||
|
|
53a80e3056 | ||
|
|
cc424b6a09 | ||
|
|
c5818c5360 | ||
|
|
70b86832b2 | ||
|
|
2df237975b | ||
|
|
60a632f584 | ||
|
|
4bdf332f30 | ||
|
|
73416452ef | ||
|
|
fc3d0ee74b | ||
|
|
4952db4960 | ||
|
|
2bdcea4bc0 | ||
|
|
f2bed231ff | ||
|
|
e1178bc80b | ||
|
|
d732af588c | ||
|
|
d492ecaefd | ||
|
|
bc6d8841eb | ||
|
|
901c0a61ad | ||
|
|
f2ad7bbd6a | ||
|
|
25c73b7c0d | ||
|
|
41b7c079a8 | ||
|
|
e386b091f1 | ||
|
|
52c4f360cc | ||
|
|
26690e06b0 | ||
|
|
e7a89e0ac0 | ||
|
|
eb90a50ca1 | ||
|
|
e221ea2b19 | ||
|
|
abd5f9e985 | ||
|
|
2f9a4f99be | ||
|
|
0cf9fcef97 | ||
|
|
3f7f4613d6 | ||
|
|
4a8501a6e5 | ||
|
|
4ae1775644 | ||
|
|
80cd338e77 | ||
|
|
cd8f785ce6 | ||
|
|
4f4f7bd633 | ||
|
|
c2413fd7fa | ||
|
|
ed596fe700 | ||
|
|
e580fb8b5c | ||
|
|
5ba7a906d4 | ||
|
|
c6159615d7 | ||
|
|
b2f63cf345 | ||
|
|
fbd7bb073e | ||
|
|
42fec4fb9c | ||
|
|
bcd7f0dc88 | ||
|
|
44b5dc9446 | ||
|
|
4fd8732160 | ||
|
|
51675e0cd2 | ||
|
|
36bddbff06 | ||
|
|
e8c15f10e2 | ||
|
|
8f870146c6 | ||
|
|
91c0d052df | ||
|
|
dfb5e7d768 | ||
|
|
03fb6f5c29 | ||
|
|
11176a7593 | ||
|
|
8a7b8ff2ac | ||
|
|
f61e1ab509 | ||
|
|
3e75d97b8a | ||
|
|
8588d34fbe | ||
|
|
f4952764d4 | ||
|
|
52d38105ae | ||
|
|
8fa6c1d027 | ||
|
|
972b999195 | ||
|
|
fe6bc5530e | ||
|
|
d5d7093ca3 | ||
|
|
52d30d5b6e | ||
|
|
2a25673659 | ||
|
|
94838cf7d1 | ||
|
|
9214bcac94 | ||
|
|
3be6887df6 | ||
|
|
33374693c9 | ||
|
|
80516f1f00 | ||
|
|
9253af6a19 | ||
|
|
58f1d51c7f | ||
|
|
05d6f614bc | ||
|
|
6e2bb1b2f5 | ||
|
|
f3a6e1028e | ||
|
|
f182b4600d | ||
|
|
b7983e0906 | ||
|
|
6c988150e4 | ||
|
|
b88a39e273 | ||
|
|
ec6b258750 | ||
|
|
ea1fabdfdd | ||
|
|
af4f86743b | ||
|
|
ed492d5b94 | ||
|
|
db00a6b7e7 | ||
|
|
864f0877f6 | ||
|
|
d7062f3d9d | ||
|
|
3051032360 | ||
|
|
54c575efd1 | ||
|
|
e6bc47e6ba | ||
|
|
6244a8db04 | ||
|
|
3f2c1ea538 | ||
|
|
aac947ee4d | ||
|
|
594d9be859 | ||
|
|
80ae2543e5 | ||
|
|
94d176c3d1 | ||
|
|
192d9f785f | ||
|
|
398ffa045b | ||
|
|
e67538fb7c | ||
|
|
025db7ffe7 | ||
|
|
f9c958c80c | ||
|
|
652897a3cb | ||
|
|
3426449315 | ||
|
|
8e19b0249d | ||
|
|
bba034eeca | ||
|
|
31f2767038 | ||
|
|
e0571044ae | ||
|
|
0f00286641 | ||
|
|
9cc166d3cf | ||
|
|
3ad1ed5aa6 | ||
|
|
8ee84c2462 | ||
|
|
eca108032f | ||
|
|
3bfe67f784 | ||
|
|
c27191de6b | ||
|
|
31f626c877 | ||
|
|
b081db7020 |
No files matched your search
@@ -51,8 +51,8 @@
|
||||
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue or discussion:
|
||||
- Link to documentation pull request:
|
||||
- Link to developer documentation pull request:
|
||||
- Link to [documentation][docs-repository] pull request:
|
||||
- Link to [contributors documentation][contributors-repository] pull request:
|
||||
- Link to backend pull request:
|
||||
|
||||
## Checklist
|
||||
@@ -63,7 +63,8 @@
|
||||
for before merging your code.
|
||||
|
||||
AI tools are welcome, but contributors are responsible for *fully*
|
||||
understanding the code before submitting a PR.
|
||||
understanding the code before submitting a PR. Please follow our AI policy:
|
||||
https://contributors.home-assistant.io/start/ai-policy/
|
||||
-->
|
||||
|
||||
- [ ] I understand the code I am submitting and can explain how it works.
|
||||
@@ -105,4 +106,5 @@ To help with the load of incoming pull requests:
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[contributors-repository]: https://github.com/home-assistant/contributors.home-assistant.io
|
||||
[perfect-pr]: https://contributors.home-assistant.io/start/pull-requests/#before-you-open-it
|
||||
@@ -2,13 +2,13 @@
|
||||
"_comment": "Initial JS budget (raw/uncompressed bytes) for the cold-load critical entrypoints. Enforced by build-scripts/check-bundle-size.cjs in CI. Re-seed after an intentional change with `--update --headroom=<percent>`.",
|
||||
"frontend-modern": {
|
||||
"app": 627654,
|
||||
"core": 55147,
|
||||
"core": 57901,
|
||||
"authorize": 593862,
|
||||
"onboarding": 709255
|
||||
},
|
||||
"frontend-legacy": {
|
||||
"app": 724225,
|
||||
"core": 157026,
|
||||
"core": 163115,
|
||||
"authorize": 693913,
|
||||
"onboarding": 812415
|
||||
}
|
||||
|
||||
@@ -117,6 +117,16 @@ module.exports.swcOptions = () => ({
|
||||
},
|
||||
});
|
||||
|
||||
const corejsPolyfills = (exclude = []) => [
|
||||
"babel-plugin-polyfill-corejs3",
|
||||
{
|
||||
method: "usage-global",
|
||||
version: dependencies["core-js"],
|
||||
shippedProposals: true,
|
||||
exclude,
|
||||
},
|
||||
];
|
||||
|
||||
module.exports.babelOptions = ({ latestBuild, isTestBuild, sw, worker }) => ({
|
||||
babelrc: false,
|
||||
compact: false,
|
||||
@@ -138,14 +148,7 @@ module.exports.babelOptions = ({ latestBuild, isTestBuild, sw, worker }) => ({
|
||||
// Inject Core-JS polyfills on demand. Babel 8 removed preset-env's
|
||||
// `useBuiltIns`/`corejs` options, so the equivalent polyfill provider is
|
||||
// configured directly here (`usage-global` matches the old `useBuiltIns: "usage"`).
|
||||
[
|
||||
"babel-plugin-polyfill-corejs3",
|
||||
{
|
||||
method: "usage-global",
|
||||
version: dependencies["core-js"],
|
||||
shippedProposals: true,
|
||||
},
|
||||
],
|
||||
corejsPolyfills(),
|
||||
[
|
||||
path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"),
|
||||
{
|
||||
@@ -191,6 +194,16 @@ module.exports.babelOptions = ({ latestBuild, isTestBuild, sw, worker }) => ({
|
||||
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
||||
],
|
||||
},
|
||||
{
|
||||
// It only constructs `Uint8Array`s, but any reference to one pulls in
|
||||
// every typed array polyfill, and this lands in the `core` entrypoint.
|
||||
test: /[\\/]node_modules[\\/]home-assistant-js-websocket[\\/]/,
|
||||
plugins: [
|
||||
corejsPolyfills([
|
||||
/^(es|esnext)\.(array-buffer|typed-array|uint8-array)\./,
|
||||
]),
|
||||
],
|
||||
},
|
||||
{
|
||||
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
|
||||
// Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
|
||||
import type { Auth, Connection } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
@@ -215,7 +214,7 @@ class HcCast extends LitElement {
|
||||
this.castManager.requestSession();
|
||||
}
|
||||
|
||||
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
|
||||
private async _handlePickView(ev: CustomEvent<{ index: number }>) {
|
||||
const path = this.lovelaceViews?.[ev.detail.index]?.path ?? ev.detail.index;
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
ZHANetworkBackup,
|
||||
ZHANetworkSettings,
|
||||
} from "../../../../../src/data/zha";
|
||||
import { ZHADeviceType } from "../../../../../src/data/zha";
|
||||
import type { MockHomeAssistant } from "../../../../../src/fake_data/provide_hass";
|
||||
import { minutesAgo } from "../helpers";
|
||||
|
||||
@@ -56,7 +57,7 @@ const DEVICES: ZHADevice[] = [
|
||||
device_reg_id: "zha-coordinator",
|
||||
user_given_name: "Home Assistant Connect ZBT-1",
|
||||
power_source: "Mains",
|
||||
device_type: "Coordinator",
|
||||
device_type: ZHADeviceType.Coordinator,
|
||||
active_coordinator: true,
|
||||
signature: {},
|
||||
neighbors: [
|
||||
@@ -83,7 +84,7 @@ const DEVICES: ZHADevice[] = [
|
||||
device_reg_id: "zha-porch-light",
|
||||
user_given_name: "Porch light",
|
||||
power_source: "Mains",
|
||||
device_type: "Router",
|
||||
device_type: ZHADeviceType.Router,
|
||||
active_coordinator: false,
|
||||
signature: {},
|
||||
neighbors: [
|
||||
@@ -110,7 +111,7 @@ const DEVICES: ZHADevice[] = [
|
||||
device_reg_id: "zha-hall-motion",
|
||||
user_given_name: "Hall motion",
|
||||
power_source: "Battery",
|
||||
device_type: "EndDevice",
|
||||
device_type: ZHADeviceType.EndDevice,
|
||||
active_coordinator: false,
|
||||
signature: {},
|
||||
neighbors: [neighbor(PORCH_IEEE, "0x1a2b", "142", "Parent", "1")],
|
||||
@@ -133,7 +134,7 @@ const DEVICES: ZHADevice[] = [
|
||||
device_reg_id: "zha-tv-plug",
|
||||
user_given_name: "TV plug",
|
||||
power_source: "Mains",
|
||||
device_type: "Router",
|
||||
device_type: ZHADeviceType.Router,
|
||||
active_coordinator: false,
|
||||
signature: {},
|
||||
neighbors: [neighbor(COORDINATOR_IEEE, "0x0000", "198", "Parent", "0")],
|
||||
@@ -156,7 +157,7 @@ const DEVICES: ZHADevice[] = [
|
||||
device_reg_id: "zha-kitchen-switch",
|
||||
user_given_name: "Kitchen switch",
|
||||
power_source: "Battery",
|
||||
device_type: "EndDevice",
|
||||
device_type: ZHADeviceType.EndDevice,
|
||||
active_coordinator: false,
|
||||
signature: {},
|
||||
neighbors: [neighbor(PORCH_IEEE, "0x1a2b", "186", "Parent", "1")],
|
||||
@@ -179,7 +180,7 @@ const DEVICES: ZHADevice[] = [
|
||||
device_reg_id: "zha-landing-sensor",
|
||||
user_given_name: "Landing sensor",
|
||||
power_source: "Battery",
|
||||
device_type: "EndDevice",
|
||||
device_type: ZHADeviceType.EndDevice,
|
||||
active_coordinator: false,
|
||||
signature: {},
|
||||
neighbors: [neighbor(OFFICE_IEEE, "0x7a8b", "164", "Parent", "2")],
|
||||
@@ -202,7 +203,7 @@ const DEVICES: ZHADevice[] = [
|
||||
device_reg_id: "zha-garage-contact",
|
||||
user_given_name: "Garage contact",
|
||||
power_source: "Battery",
|
||||
device_type: "EndDevice",
|
||||
device_type: ZHADeviceType.EndDevice,
|
||||
active_coordinator: false,
|
||||
signature: {},
|
||||
neighbors: [neighbor(OFFICE_IEEE, "0x7a8b", "118", "Parent", "2")],
|
||||
@@ -225,7 +226,7 @@ const DEVICES: ZHADevice[] = [
|
||||
device_reg_id: "zha-office-plug",
|
||||
user_given_name: "Office plug",
|
||||
power_source: "Mains",
|
||||
device_type: "Router",
|
||||
device_type: ZHADeviceType.Router,
|
||||
active_coordinator: false,
|
||||
signature: {},
|
||||
neighbors: [
|
||||
@@ -593,7 +594,8 @@ export const mockZha = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("zha/devices/bindable", (msg: { ieee: string }) =>
|
||||
DEVICES.filter(
|
||||
(candidate) =>
|
||||
candidate.device_type === "Router" && candidate.ieee !== msg.ieee
|
||||
candidate.device_type === ZHADeviceType.Router &&
|
||||
candidate.ieee !== msg.ieee
|
||||
)
|
||||
);
|
||||
hass.mockWS("zha/devices/bind", () => undefined);
|
||||
|
||||
@@ -31,7 +31,7 @@ const exposedEntities: Record<string, ExposeEntitySettings> = {
|
||||
|
||||
export const mockExpose = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("homeassistant/expose_entity/list", () => ({
|
||||
exposed_entities: exposedEntities,
|
||||
exposed_entities: structuredClone(exposedEntities),
|
||||
}));
|
||||
hass.mockWS(
|
||||
"homeassistant/expose_new_entities/get",
|
||||
@@ -39,6 +39,26 @@ export const mockExpose = (hass: MockHomeAssistant) => {
|
||||
expose_new: msg.assistant !== "cloud.google_assistant",
|
||||
})
|
||||
);
|
||||
hass.mockWS("homeassistant/expose_entity", () => null);
|
||||
hass.mockWS(
|
||||
"homeassistant/expose_entity",
|
||||
(msg: {
|
||||
assistants: (keyof ExposeEntitySettings)[];
|
||||
entity_ids: string[];
|
||||
should_expose: boolean;
|
||||
}) => {
|
||||
const changedEntityIds = msg.entity_ids.filter((entityId) => {
|
||||
const settings = (exposedEntities[entityId] ??= {});
|
||||
let changed = false;
|
||||
for (const assistant of msg.assistants) {
|
||||
if (Boolean(settings[assistant]) !== msg.should_expose) {
|
||||
changed = true;
|
||||
}
|
||||
settings[assistant] = msg.should_expose;
|
||||
}
|
||||
return changed;
|
||||
});
|
||||
return { changed_entity_ids: changedEntityIds };
|
||||
}
|
||||
);
|
||||
hass.mockWS("homeassistant/expose_new_entities/set", () => null);
|
||||
};
|
||||
@@ -48,6 +48,36 @@ export const createMediaPlayerEntities = () => [
|
||||
group_members: ["media_player.playing", "media_player.stream_playing"],
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "media_player.music_all_features",
|
||||
state: "playing",
|
||||
attributes: {
|
||||
friendly_name: "Playing The Music (all features)",
|
||||
media_content_type: "music",
|
||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||
media_artist: "Technohead",
|
||||
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + On + Off +
|
||||
// Play Media + Volume Step + Select Source + Stop + Clear + Play + Shuffle Set +
|
||||
// Sound Mode + Browse Media + Repeat Set + Grouping
|
||||
supported_features: 1048511,
|
||||
entity_picture: "/images/album_cover.jpg",
|
||||
media_duration: 300,
|
||||
media_position: 0,
|
||||
media_position_updated_at: new Date(
|
||||
// 23 seconds in
|
||||
new Date().getTime() - 23000
|
||||
).toISOString(),
|
||||
volume_level: 0.5,
|
||||
is_volume_muted: false,
|
||||
shuffle: false,
|
||||
repeat: "off",
|
||||
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||
source: "AirPlay",
|
||||
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
|
||||
sound_mode: "Music",
|
||||
group_members: ["media_player.playing", "media_player.stream_playing"],
|
||||
},
|
||||
},
|
||||
{
|
||||
entity_id: "media_player.stream_playing",
|
||||
state: "playing",
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { mdiFan, mdiFanSpeed1, mdiFanSpeed2, mdiFanSpeed3 } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-card";
|
||||
import type { HaDropdownSelectEvent } from "../../../../src/components/ha-dropdown";
|
||||
import "../../../../src/components/ha-control-select-menu";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
import type { SelectOption } from "../../../../src/components/ha-control-select-menu";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
|
||||
interface SelectMenuOptions {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface SelectMenu {
|
||||
label: string;
|
||||
icon: string;
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
options: SelectMenuOptions[];
|
||||
options: SelectOption[];
|
||||
}
|
||||
|
||||
const selects: SelectMenu[] = [
|
||||
@@ -48,17 +44,17 @@ const selects: SelectMenu[] = [
|
||||
{
|
||||
value: "low",
|
||||
label: "Low",
|
||||
icon: mdiFanSpeed1,
|
||||
iconPath: mdiFanSpeed1,
|
||||
},
|
||||
{
|
||||
value: "medium",
|
||||
label: "Medium",
|
||||
icon: mdiFanSpeed2,
|
||||
iconPath: mdiFanSpeed2,
|
||||
},
|
||||
{
|
||||
value: "high",
|
||||
label: "High",
|
||||
icon: mdiFanSpeed3,
|
||||
iconPath: mdiFanSpeed3,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -72,6 +68,8 @@ const selects: SelectMenu[] = [
|
||||
|
||||
@customElement("demo-components-ha-control-select-menu")
|
||||
export class DemoHaControlSelectMenu extends LitElement {
|
||||
@state() private _values: Record<string, string> = {};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card>
|
||||
@@ -81,31 +79,13 @@ export class DemoHaControlSelectMenu extends LitElement {
|
||||
<div class="card-content">
|
||||
<ha-control-select-menu
|
||||
.label=${select.label}
|
||||
.value=${this._values[select.label]}
|
||||
.options=${select.options}
|
||||
?disabled=${select.disabled}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
data-label=${select.label}
|
||||
@wa-select=${this._handleSelect}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${select.icon}></ha-svg-icon>
|
||||
${select.options.map(
|
||||
(option) => html`
|
||||
<ha-list-item
|
||||
.value=${option.value}
|
||||
.graphic=${option.icon ? "icon" : undefined}
|
||||
>
|
||||
${
|
||||
option.icon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${option.icon}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${option.label ?? option.value}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-control-select-menu>
|
||||
</div>
|
||||
`
|
||||
@@ -114,6 +94,17 @@ export class DemoHaControlSelectMenu extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSelect(
|
||||
ev: HaDropdownSelectEvent & HASSDomCurrentTargetEvent<HTMLElement>
|
||||
) {
|
||||
const label = ev.currentTarget.dataset.label;
|
||||
const value = ev.detail.item.value;
|
||||
if (!label || !value) {
|
||||
return;
|
||||
}
|
||||
this._values = { ...this._values, [label]: value };
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { customElement, state } from "lit/decorators";
|
||||
import { formatDateTimeNumeric } from "../../../../src/common/datetime/format_date_time";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import "../../../../src/components/ha-list";
|
||||
import type { FrontendLocaleData } from "../../../../src/data/translation";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -49,7 +48,7 @@ export class DemoDateTimeDateTimeNumeric extends LitElement {
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<ha-list>
|
||||
<div>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
@@ -96,7 +95,7 @@ export class DemoDateTimeDateTimeNumeric extends LitElement {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { customElement, state } from "lit/decorators";
|
||||
import { formatDateTimeWithSeconds } from "../../../../src/common/datetime/format_date_time";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import "../../../../src/components/ha-list";
|
||||
import type { FrontendLocaleData } from "../../../../src/data/translation";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -49,7 +48,7 @@ export class DemoDateTimeDateTimeSeconds extends LitElement {
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<ha-list>
|
||||
<div>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
@@ -96,7 +95,7 @@ export class DemoDateTimeDateTimeSeconds extends LitElement {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { customElement, state } from "lit/decorators";
|
||||
import { formatShortDateTimeWithYear } from "../../../../src/common/datetime/format_date_time";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import "../../../../src/components/ha-list";
|
||||
import type { FrontendLocaleData } from "../../../../src/data/translation";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -49,7 +48,7 @@ export class DemoDateTimeDateTimeShortYear extends LitElement {
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<ha-list>
|
||||
<div>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
@@ -96,7 +95,7 @@ export class DemoDateTimeDateTimeShortYear extends LitElement {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { customElement, state } from "lit/decorators";
|
||||
import { formatShortDateTime } from "../../../../src/common/datetime/format_date_time";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import "../../../../src/components/ha-list";
|
||||
import type { FrontendLocaleData } from "../../../../src/data/translation";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -49,7 +48,7 @@ export class DemoDateTimeDateTimeShort extends LitElement {
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<ha-list>
|
||||
<div>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
@@ -96,7 +95,7 @@ export class DemoDateTimeDateTimeShort extends LitElement {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { customElement, state } from "lit/decorators";
|
||||
import { formatDateTime } from "../../../../src/common/datetime/format_date_time";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import "../../../../src/components/ha-list";
|
||||
import type { FrontendLocaleData } from "../../../../src/data/translation";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -49,7 +48,7 @@ export class DemoDateTimeDateTime extends LitElement {
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<ha-list>
|
||||
<div>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
@@ -96,7 +95,7 @@ export class DemoDateTimeDateTime extends LitElement {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { formatDateNumeric } from "../../../../src/common/datetime/format_date";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-list";
|
||||
import type { FrontendLocaleData } from "../../../../src/data/translation";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -27,7 +26,7 @@ export class DemoDateTimeDate extends LitElement {
|
||||
};
|
||||
const date = new Date();
|
||||
return html`
|
||||
<ha-list>
|
||||
<div>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
@@ -86,7 +85,7 @@ export class DemoDateTimeDate extends LitElement {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { customElement, state } from "lit/decorators";
|
||||
import { formatTimeWithSeconds } from "../../../../src/common/datetime/format_time";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import "../../../../src/components/ha-list";
|
||||
import type { FrontendLocaleData } from "../../../../src/data/translation";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -49,7 +48,7 @@ export class DemoDateTimeTimeSeconds extends LitElement {
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<ha-list>
|
||||
<div>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
@@ -96,7 +95,7 @@ export class DemoDateTimeTimeSeconds extends LitElement {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { customElement, state } from "lit/decorators";
|
||||
import { formatTimeWeekday } from "../../../../src/common/datetime/format_time";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import "../../../../src/components/ha-list";
|
||||
import type { FrontendLocaleData } from "../../../../src/data/translation";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -49,7 +48,7 @@ export class DemoDateTimeTimeWeekday extends LitElement {
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<ha-list>
|
||||
<div>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
@@ -96,7 +95,7 @@ export class DemoDateTimeTimeWeekday extends LitElement {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { customElement, state } from "lit/decorators";
|
||||
import { formatTime } from "../../../../src/common/datetime/format_time";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import "../../../../src/components/ha-list";
|
||||
import type { FrontendLocaleData } from "../../../../src/data/translation";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -49,7 +48,7 @@ export class DemoDateTimeTime extends LitElement {
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<ha-list>
|
||||
<div>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
@@ -96,7 +95,7 @@ export class DemoDateTimeTime extends LitElement {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,13 @@ const CONFIGS = [
|
||||
entity: "media_player.music_playing",
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Playing Music (all features)",
|
||||
config: {
|
||||
type: "media-control",
|
||||
entity: "media_player.music_all_features",
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "Playing Stream",
|
||||
config: {
|
||||
|
||||
@@ -81,6 +81,15 @@ const CONFIGS = [
|
||||
state_color: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "With custom color",
|
||||
config: {
|
||||
type: "sensor",
|
||||
entity: "sensor.outside_temperature",
|
||||
color: "teal",
|
||||
graph: "line",
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: "With name, icon and unit",
|
||||
config: {
|
||||
|
||||
+8
-8
@@ -63,7 +63,7 @@
|
||||
"@formatjs/intl-numberformat": "9.4.3",
|
||||
"@formatjs/intl-pluralrules": "6.3.16",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.16",
|
||||
"@home-assistant/webawesome": "3.14.0-ha.1",
|
||||
"@home-assistant/webawesome": "3.14.0-ha.2",
|
||||
"@lezer/common": "1.5.4",
|
||||
"@lezer/highlight": "1.2.5",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
@@ -78,8 +78,8 @@
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@tsparticles/engine": "4.4.0",
|
||||
"@tsparticles/preset-links": "4.4.0",
|
||||
"@tsparticles/engine": "4.5.0",
|
||||
"@tsparticles/preset-links": "4.5.0",
|
||||
"@versatiles/style": "7.0.1",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vvo/tzdb": "6.201.1",
|
||||
@@ -101,7 +101,7 @@
|
||||
"fullcalendar": "7.1.3",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.7.3",
|
||||
"home-assistant-js-websocket": "9.7.1",
|
||||
"home-assistant-js-websocket": "9.8.1",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "12.1.4",
|
||||
"js-yaml": "5.5.0",
|
||||
@@ -109,8 +109,8 @@
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"maplibre-gl": "6.15.0",
|
||||
"marked": "18.1.1",
|
||||
"maplibre-gl": "6.16.0",
|
||||
"marked": "18.1.2",
|
||||
"memoize-one": "6.0.0",
|
||||
"nanoid": "6.0.2",
|
||||
"node-vibrant": "4.0.4",
|
||||
@@ -132,7 +132,7 @@
|
||||
"workbox-precaching": "7.4.1",
|
||||
"workbox-routing": "7.4.1",
|
||||
"workbox-strategies": "7.4.1",
|
||||
"xss": "1.0.16",
|
||||
"xss": "1.0.17",
|
||||
"zrender": "6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -154,7 +154,7 @@
|
||||
"@playwright/test": "1.64.1",
|
||||
"@rsdoctor/rspack-plugin": "1.6.5",
|
||||
"@rspack/core": "2.2.9",
|
||||
"@rspack/dev-server": "2.2.1",
|
||||
"@rspack/dev-server": "2.2.2",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "7.0.1",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
|
||||
@@ -13,3 +13,27 @@ index 95366713472f3ec5f3649547bb1fe202b806a2e6..e91f99229d554e79077486e1caf3602c
|
||||
originX: e.pinchX,
|
||||
originY: e.pinchY,
|
||||
isAvailableBehavior: null
|
||||
diff --git a/lib/label/LabelManager.js b/lib/label/LabelManager.js
|
||||
index 3d233be98b14223168726ebb3d9cd9b768fa2554..d6abfe5484ea8c2f0cb3a9d7c7c2fe31e7516fc7 100644
|
||||
--- a/lib/label/LabelManager.js
|
||||
+++ b/lib/label/LabelManager.js
|
||||
@@ -43,6 +43,7 @@
|
||||
*/
|
||||
// TODO: move labels out of viewport.
|
||||
import { BoundingRect, updateProps, initProps, isElementRemoved } from '../util/graphic.js';
|
||||
+import { getAnimationConfig } from '../animation/basicTransition.js';
|
||||
import { getECData } from '../util/innerStore.js';
|
||||
import { parsePercent } from '../util/number.js';
|
||||
import Transformable from 'zrender/lib/core/Transformable.js';
|
||||
@@ -395,6 +396,11 @@ var LabelManager = /** @class */function () {
|
||||
textEl.attr(layoutStore.oldLayoutEmphasis);
|
||||
}
|
||||
}
|
||||
+ // An instant update, e.g. on roam, stops all animations and would leave the fade in half done
|
||||
+ var updateConfig = getAnimationConfig('update', seriesModel, dataIndex);
|
||||
+ if (!(updateConfig && updateConfig.duration > 0)) {
|
||||
+ textEl.stopAnimation('enter', true);
|
||||
+ }
|
||||
updateProps(textEl, newProps, seriesModel, dataIndex);
|
||||
}
|
||||
layoutStore.oldLayout = newProps;
|
||||
Generated
+153
-153
@@ -171,7 +171,7 @@ overrides:
|
||||
|
||||
patchedDependencies:
|
||||
'@material/[email protected]': 9450e509a86887d8f68ca9cf3f81e6ed70e94e49de13c5f6f20f94519ad4a987
|
||||
[email protected]: b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0
|
||||
[email protected]: 4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5
|
||||
[email protected]: f2ec2ae6877dfc201c542b4f9ded1d03831258bf328ae7bba876d14ce4508a06
|
||||
[email protected]: cdc7ece96d53357d38ff3c124a2553f86e3a5dc333d641d223eb6f07e28275f0
|
||||
[email protected]: 75e6346ad8d8af554c167f77ac7d9b9652f010398efdc6b399925a4795133099
|
||||
@@ -250,8 +250,8 @@ importers:
|
||||
specifier: 12.3.16
|
||||
version: 12.3.16
|
||||
'@home-assistant/webawesome':
|
||||
specifier: 3.14.0-ha.1
|
||||
version: 3.14.0-ha.1(@floating-ui/[email protected])(@types/[email protected])
|
||||
specifier: 3.14.0-ha.2
|
||||
version: 3.14.0-ha.2(@floating-ui/[email protected])(@types/[email protected])
|
||||
'@lezer/common':
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4
|
||||
@@ -278,7 +278,7 @@ importers:
|
||||
version: 1.0.3
|
||||
'@maplibre/maplibre-gl-leaflet':
|
||||
specifier: 0.1.4
|
||||
version: 0.1.4(@types/[email protected])([email protected])([email protected]5.0)
|
||||
version: 0.1.4(@types/[email protected])([email protected])([email protected]6.0)
|
||||
'@material/mwc-list':
|
||||
specifier: 0.27.0
|
||||
version: 0.27.0(patch_hash=9450e509a86887d8f68ca9cf3f81e6ed70e94e49de13c5f6f20f94519ad4a987)
|
||||
@@ -295,14 +295,14 @@ importers:
|
||||
specifier: 0.5.23
|
||||
version: 0.5.23
|
||||
'@tsparticles/engine':
|
||||
specifier: 4.4.0
|
||||
version: 4.4.0
|
||||
specifier: 4.5.0
|
||||
version: 4.5.0
|
||||
'@tsparticles/preset-links':
|
||||
specifier: 4.4.0
|
||||
version: 4.4.0
|
||||
specifier: 4.5.0
|
||||
version: 4.5.0
|
||||
'@versatiles/style':
|
||||
specifier: 7.0.1
|
||||
version: 7.0.1([email protected]5.0)
|
||||
version: 7.0.1([email protected]6.0)
|
||||
'@vibrant/color':
|
||||
specifier: 4.0.4
|
||||
version: 4.0.4
|
||||
@@ -347,10 +347,10 @@ importers:
|
||||
version: 0.0.1
|
||||
echarts:
|
||||
specifier: 6.1.0
|
||||
version: 6.1.0(patch_hash=b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0)
|
||||
version: 6.1.0(patch_hash=4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5)
|
||||
echarts-extension-chart2music:
|
||||
specifier: 0.1.1
|
||||
version: 0.1.1([email protected](patch_hash=b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0))([email protected])
|
||||
version: 0.1.1([email protected](patch_hash=4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5))([email protected])
|
||||
element-internals-polyfill:
|
||||
specifier: 3.0.2
|
||||
version: 3.0.2
|
||||
@@ -364,8 +364,8 @@ importers:
|
||||
specifier: 1.7.3
|
||||
version: 1.7.3
|
||||
home-assistant-js-websocket:
|
||||
specifier: 9.7.1
|
||||
version: 9.7.1
|
||||
specifier: 9.8.1
|
||||
version: 9.8.1
|
||||
idb-keyval:
|
||||
specifier: 6.3.0
|
||||
version: 6.3.0
|
||||
@@ -388,11 +388,11 @@ importers:
|
||||
specifier: 3.3.3
|
||||
version: 3.3.3
|
||||
maplibre-gl:
|
||||
specifier: 6.15.0
|
||||
version: 6.15.0
|
||||
specifier: 6.16.0
|
||||
version: 6.16.0
|
||||
marked:
|
||||
specifier: 18.1.1
|
||||
version: 18.1.1
|
||||
specifier: 18.1.2
|
||||
version: 18.1.2
|
||||
memoize-one:
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0
|
||||
@@ -457,8 +457,8 @@ importers:
|
||||
specifier: 7.4.1
|
||||
version: 7.4.1
|
||||
xss:
|
||||
specifier: 1.0.16
|
||||
version: 1.0.16
|
||||
specifier: 1.0.17
|
||||
version: 1.0.17
|
||||
zrender:
|
||||
specifier: 6.1.0
|
||||
version: 6.1.0
|
||||
@@ -518,8 +518,8 @@ importers:
|
||||
specifier: 2.2.9
|
||||
version: 2.2.9(@swc/[email protected])
|
||||
'@rspack/dev-server':
|
||||
specifier: 2.2.1
|
||||
version: 2.2.1(@rspack/[email protected](@swc/[email protected]))
|
||||
specifier: 2.2.2
|
||||
version: 2.2.2(@rspack/[email protected](@swc/[email protected]))
|
||||
'@types/babel__plugin-transform-runtime':
|
||||
specifier: 7.9.5
|
||||
version: 7.9.5
|
||||
@@ -2024,8 +2024,8 @@ packages:
|
||||
resolution: {integrity: sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
'@home-assistant/[email protected].1':
|
||||
resolution: {integrity: sha512-h/HVT3LnDIk9rQ2+xAm0WTXFG89sV1KEAqLwjxl5tYBDAftQZCxV+5adchRref8anvBtp1opctjxHtuNBwgQ2g==}
|
||||
'@home-assistant/[email protected].2':
|
||||
resolution: {integrity: sha512-EX1qiX5h3LAoTjuE3rgfuRlNq3Rds5tGrgDGlHoFbxhXaCSTTtuYU6tdR8vgOXKGUDi5gBDrcoNdLYKFXSrknA==}
|
||||
engines: {node: '>=14.17.0'}
|
||||
|
||||
'@html-eslint/[email protected]':
|
||||
@@ -2258,8 +2258,8 @@ packages:
|
||||
'@maplibre/[email protected]':
|
||||
resolution: {integrity: sha512-7M2O7ABM80Oi8XPkfkrjKB8mOBIxGoVgkA52Mu4RMWoBvvMdagQ522+WxVYujZsMpqIr3BpzrTbXrm4k8lCzCQ==}
|
||||
|
||||
'@maplibre/[email protected].2':
|
||||
resolution: {integrity: sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==}
|
||||
'@maplibre/[email protected].3':
|
||||
resolution: {integrity: sha512-dEe5XUd64RQbxNxy2OzIkJnwNDWjVmSXjCd0A7zLirm7QJ/WWbmtDFIkfsiM4yt1cplqzG6oSivRYeWx1SM1yg==}
|
||||
|
||||
'@marijn/[email protected]':
|
||||
resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
|
||||
@@ -2990,8 +2990,8 @@ packages:
|
||||
'@swc/helpers':
|
||||
optional: true
|
||||
|
||||
'@rspack/[email protected].3':
|
||||
resolution: {integrity: sha512-GxnGj9jy76G3eCPyZei81fwKLAMLZaPEEqFz1/QDYquhwi/qYZX5fekFJ1XVpuwxGEK9KSX3hxZylfwrs4cmLA==}
|
||||
'@rspack/[email protected].6':
|
||||
resolution: {integrity: sha512-Rcx7dlDZQ9ddUJGuutFvnSeAKl+ZOc1fnC3htibs8yew3zMD9VgvSUe4MAwEn94Gb390l5heA+s/j5KhoSG5MQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
'@rspack/core': ^2.0.0
|
||||
@@ -2999,8 +2999,8 @@ packages:
|
||||
'@rspack/core':
|
||||
optional: true
|
||||
|
||||
'@rspack/[email protected].1':
|
||||
resolution: {integrity: sha512-Il8HbYGK3rH5rM0XmUOsOGZVw69BKRpBZ61P28Fy8ry35CtfniDBWtbo/rvJtIT/sK0qwijmNrhF498ltsV+uA==}
|
||||
'@rspack/[email protected].2':
|
||||
resolution: {integrity: sha512-pK4V9YJW8dkfjy6WYJ2T6DrPkcIn2yjA1HiaU146141j8UCxHok5lsplyqudLzerOhvwqMrNFmnsuXLnFbfn4Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
'@rspack/core': ^2.0.0
|
||||
@@ -3156,85 +3156,85 @@ packages:
|
||||
resolution: {integrity: sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@tsparticles/animation-utils@4.4.0':
|
||||
resolution: {integrity: sha512-yl4iE9SSuJ2n8B096t/vn4bWN+wWIkDm11OfTsIM3/64bJLB/5T92n6iOm/XLC3r6g8M8XuDi6ntTnpOcwsGxg==}
|
||||
'@tsparticles/animation-utils@4.5.0':
|
||||
resolution: {integrity: sha512-jIcPbhaA7X9gIEzUOsw63Vn7558ZLRbPeAodfzvIEo+VYhmnKOCrnf7e91THZ2XoFYjRQkkOt94uJfT8bma1TQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/basic@4.4.0':
|
||||
resolution: {integrity: sha512-lTbKBPzab0alPy+4Wm/xehrVpT1FrpEMaQcTU2eTV0c0G++5/kOINEWkC7GL7/cHovycukxB+tOMYVT54bSprA==}
|
||||
'@tsparticles/basic@4.5.0':
|
||||
resolution: {integrity: sha512-4yZucXSWNeHh5NXjJ1is4aiq1Fi8hW5gBTrlWbIm7JcqBjUxNkYCqdLKvOCR/5bnToqjtezBJWlArwBdbzuU6w==}
|
||||
|
||||
'@tsparticles/canvas-utils@4.4.0':
|
||||
resolution: {integrity: sha512-3zVr95OZ05BExgVYNT8WPqA/wFp83TiEJo66rZCZ/nZd1Ny8UCYLMKBcbUPBvGS2GB87UMK8+ygleZnlFhGTtw==}
|
||||
'@tsparticles/canvas-utils@4.5.0':
|
||||
resolution: {integrity: sha512-Yu/lxI9DzJvcvXSLlqEc7j/1B9f3A/Wa2yK6R+fn3ysDmf4CUVdjOiwa5Y65LlvvSpZMfBLbPoIgkwnCSbYaow==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/engine@4.4.0':
|
||||
resolution: {integrity: sha512-zUDykZ7qBqHYMXr6NveXg51kBlvfaQKMmWkMJ8nT7fC4dXdNayb1+TxOfAoqH9/U/u0Eyr0YE+72ai/ymS0uww==}
|
||||
'@tsparticles/engine@4.5.0':
|
||||
resolution: {integrity: sha512-ClhG2dxDU7HUAqCHGQZLgyIQRJFhuuCFCnNowSB9znuoNrxSeln4UKVBbRRv/994Eoi5OHuHuGaK9iw6ZeT21g==}
|
||||
|
||||
'@tsparticles/interaction-particles-links@4.4.0':
|
||||
resolution: {integrity: sha512-n2OkzB8BjQkzFrtiVq8O42/Tf+/Psng+K71MhJDI+czkwLo3PdmG51MCsy2w9ywCvGwWci2nwxMQ1qZL7nPONg==}
|
||||
'@tsparticles/interaction-particles-links@4.5.0':
|
||||
resolution: {integrity: sha512-L6F7eJdFVn3JsulaKrIwnl52rE9EazfMrNIUpa829PRoi/30BPOmRP469EKjdv3mntiwjUXgWFx8NDj+iQkJgw==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/plugin-interactivity': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
'@tsparticles/plugin-interactivity': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-blend@4.4.0':
|
||||
resolution: {integrity: sha512-X/SG2WIasgHqA+fLZFu6MMv3z5VY5uX4VIfKjXz1qxZjEjn0w4kzZrbtZBxXxRKdrm8Dg8Qu2mmBokNZ8Bny3g==}
|
||||
'@tsparticles/plugin-blend@4.5.0':
|
||||
resolution: {integrity: sha512-iTy4OajUDyDHyYVh2NaUZjKwzrjFfiGLiWl1I04dli417Ecmwfq/pU6leX/SIARVAqdvaeZOwFg4YmIN+zvv6w==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-hex-color@4.4.0':
|
||||
resolution: {integrity: sha512-sbRW6fi/YOS/hvFi6GCQiKI2zMCrmydrhlzqWyw2UUyg4Rr3FviQbIC2VRZrMc30Cm6kSkR3SovXIFDpZzO94Q==}
|
||||
'@tsparticles/plugin-hex-color@4.5.0':
|
||||
resolution: {integrity: sha512-yO3Rw27rR4ZpJo8IuBp0br1GXaju7iUl4u8zVD8WNcig7bbgoZqzhFhofgQYY9UPLVLyG1dnn8aODsk5Rq0X2g==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-hsl-color@4.4.0':
|
||||
resolution: {integrity: sha512-nw/7cGoU99y8veeYD7yA7ygNyrGr461V9xx+ed+jrj0LyXQbLF3d/r78ovMseoHxm4SWa9pmtoQxS3fBHIgecw==}
|
||||
'@tsparticles/plugin-hsl-color@4.5.0':
|
||||
resolution: {integrity: sha512-6AyFRVVXyHb4NrytzwKoCIUhSD/ZXIIr1av2z7cNgaLJCZ00qrur/G7FJhK1XUBNR4DV6/hYhP9ohSfrEhdZ+Q==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-interactivity@4.4.0':
|
||||
resolution: {integrity: sha512-jquoxlT4ra1Mc4glVlIfPubmchyIq5IZ25RdnvAop1uuc+6tu4N1zq5SSeyRO8MhJ94xevsNe6ozjmqvZ9CzKA==}
|
||||
'@tsparticles/plugin-interactivity@4.5.0':
|
||||
resolution: {integrity: sha512-Ydxd29SRldhjbvf5IIhu5NIfCAW2LdqSk6aN60zJb6AupV+wxztJ4/e4JL1t0ymj9992iQSVD9nQql4yzbdOwA==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-move@4.4.0':
|
||||
resolution: {integrity: sha512-o05YP6bWxljbxmsQoqbay8sn4GtHIiTQZhZmcIyxJw/q+oSCNKHWALElSAqa1H7Dp/7ltnmd4rH4pgbQtHnfEw==}
|
||||
'@tsparticles/plugin-move@4.5.0':
|
||||
resolution: {integrity: sha512-0lkYVygqc30SqBiuviI900fBKPHkJJyuZVpUBSolGSTz7J1P49PXKqy1az6f+ub42SZxFcGOChtQ5vwpZSh0Gg==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-rgb-color@4.4.0':
|
||||
resolution: {integrity: sha512-s7YlsrcjndVLiOgKNPQaPcEQmmZeZqHec0lpSbt8BgR2pQmjFvCMSzHDA/aaqZ6JX8iVqvj+bT8uqT5XQDq70Q==}
|
||||
'@tsparticles/plugin-rgb-color@4.5.0':
|
||||
resolution: {integrity: sha512-DBhtOO82K2kLaFginNEGfgfKoSXycj0eJzCHp2kSQ0HjKkpwINdK2nFh2QXfsYfjP1fQbLaIu3CuJG/VmjkDvw==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/preset-links@4.4.0':
|
||||
resolution: {integrity: sha512-skdPsg/3AgBFtv0vrn20KrPGbIa0AdZw812Avg86BW7NqVGeX3JLyowZbFRQTPU13CcUo9YXCeJccoZv6FovOw==}
|
||||
'@tsparticles/preset-links@4.5.0':
|
||||
resolution: {integrity: sha512-Itwr6ECwf3ORiroUvLAwwg8O66IT3tQ2CFdml6A0u3ze1kfBmL9JT/u9UYG4QFICT0qjE86pkXpFRv8NwWLkBw==}
|
||||
|
||||
'@tsparticles/shape-circle@4.4.0':
|
||||
resolution: {integrity: sha512-pCdCenLohHwaeeuMCw9bh1icGB+CKwBYx67L5dTBw6aig7LRvL+Kd1PKClJTgXkx8Mb1Xb7KLNmSYgh/ABRtEQ==}
|
||||
'@tsparticles/shape-circle@4.5.0':
|
||||
resolution: {integrity: sha512-6fyvghJFvUMokKIQU6p7KKwcX3pTnp6gizj/occVTnar8AxxOlXNjK1J019etwnqdLbSWU/E7Qdt7y4zCTPD1A==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-opacity@4.4.0':
|
||||
resolution: {integrity: sha512-StQAExNqeOHANRAMXHdaKKEBD1DrRwBZ3TQF6DPtERtoElj8BvMlPHq38W0TSzfvR0lFP91p2Pzo5txVzVJ3Gg==}
|
||||
'@tsparticles/updater-opacity@4.5.0':
|
||||
resolution: {integrity: sha512-808wb6KUe1wYzth/hJBMzcLDsApmES4FHOavoulyQmSJdWfiNED4LjoX2MNJg4ry56wtg9IXS7qa8XaqdekQYA==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-out-modes@4.4.0':
|
||||
resolution: {integrity: sha512-IzHaYhOVI6mBFqub1ivd/5UMkvn5Pb/j9xS8edT7XeSNYqO3UeomLJci0KE6Xsm+4K/kBLGbCRW5+Tf+kV4E0g==}
|
||||
'@tsparticles/updater-out-modes@4.5.0':
|
||||
resolution: {integrity: sha512-fOzj/2nUtwPK7A+1WN+EnQZtLPBI8++hxkgOHr+RIEGo2d9K4qOgoIxI1zpa6WHt9fSXCk4rWsjENpc++pB+JQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-paint@4.4.0':
|
||||
resolution: {integrity: sha512-v7liuaT9BwHsd1hCfJF2d93M2gD3lSfsfIfKjPihAYo73hS43Wq7Cf1F0yjEiR3yXryRGH1QmYYdhonKAvyDKw==}
|
||||
'@tsparticles/updater-paint@4.5.0':
|
||||
resolution: {integrity: sha512-BJK3De1UZW01uyIk5AJVW4ex2ejEfoU5Vqw512Ox/kGipx3qXsaROTbh/9+NbqMtRyDtYuDc5PgGWIWTyYb7TQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-size@4.4.0':
|
||||
resolution: {integrity: sha512-Qq8QjC0MMjlaHjtpWUROSqe14eKVphq92n0VR4/d4WY+pnSG1U1VJzF43Kyfb4IyV06b29F0TUROIG7Ivqk36g==}
|
||||
'@tsparticles/updater-size@4.5.0':
|
||||
resolution: {integrity: sha512-42AZGbnWUmIL7giRaerPauYHaX2Agx482+8dq3hBYvvc0vM4Pks/fhpaKtTCAGG4CJb3fvaiyRrF7T75CQVAdQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tufjs/[email protected]':
|
||||
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
|
||||
@@ -5205,8 +5205,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-MsPlx6yVW4Qv4C7mEVou4/gk/5cN2dVxOTnrNjPSmyH2f0Ln+/UkG10Ax1PE6OvrmTVPKgQYozTtBaPOuCzq7A==}
|
||||
|
||||
home-assistant-js-websocket@9.7.1:
|
||||
resolution: {integrity: sha512-zOAiyF+gUc/IsiswYdWKns6SCBbvWb8iJtMX9+Dk0wlbdfcp+I38T2Vx36krWeWMk/GPwoSiYwCs5hk/+Hsn7w==}
|
||||
home-assistant-js-websocket@9.8.1:
|
||||
resolution: {integrity: sha512-WvovbrSoYLTKfh6aY5SxSWPPy50/mLWLCihQL8G2N+2e3mTrg3QTQCAp4R32/LwbkRBDGj0XNs6JFj8e2IYEUw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
|
||||
@@ -5898,8 +5898,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==}
|
||||
|
||||
[email protected]5.0:
|
||||
resolution: {integrity: sha512-kxFetbvUHDOVaAP1CYb226Mdk2QhzIPddqM0xJTFD35wu78Mpd6NM7+H68ALjc6Oa0kAOX5EAdMKyvD2u3qfcg==}
|
||||
[email protected]6.0:
|
||||
resolution: {integrity: sha512-gApO66EMtoDZuio680RUBvskR7ahFd/NNhLN4tBAs+Y7FeT4gQV8zVFgYVMRmFxQznOEzecW19w6xOd9XjZjiA==}
|
||||
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
|
||||
|
||||
[email protected]:
|
||||
@@ -5907,8 +5907,8 @@ packages:
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
[email protected].1:
|
||||
resolution: {integrity: sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==}
|
||||
[email protected].2:
|
||||
resolution: {integrity: sha512-DfOnvLAQ6WYeWxWO1u4RyFOeqyHup6dks5PPqnoFiooU7ipRvidO/2j8plhAHIiZg574E9ZlzSjUiPMLAzFkgg==}
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
@@ -7776,8 +7776,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
|
||||
[email protected]6:
|
||||
resolution: {integrity: sha512-CsEvZ0QNH9vVgf3qldg+mJmmIwreZZEu/JS1SgldJrJqZgwyMrmIfENt1BJlkj/cPxO0c+R80kB5OMRtUPgqpQ==}
|
||||
[email protected]7:
|
||||
resolution: {integrity: sha512-+SEr5IDSgTBnhT6bnnf0mVRHT91+HU517zkahLNhkWLz8retCgeR1YDT4OknLiExA5+chn2Wy7z3KyjvnD8A2w==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -9483,7 +9483,7 @@ snapshots:
|
||||
dependencies:
|
||||
is-negated-glob: 1.0.0
|
||||
|
||||
'@home-assistant/[email protected].1(@floating-ui/[email protected])(@types/[email protected])':
|
||||
'@home-assistant/[email protected].2(@floating-ui/[email protected])(@types/[email protected])':
|
||||
dependencies:
|
||||
'@ctrl/tinycolor': 4.1.0
|
||||
'@floating-ui/dom': 1.8.0
|
||||
@@ -9784,11 +9784,11 @@ snapshots:
|
||||
dependencies:
|
||||
kdbush: 4.1.0
|
||||
|
||||
'@maplibre/[email protected](@types/[email protected])([email protected])([email protected]5.0)':
|
||||
'@maplibre/[email protected](@types/[email protected])([email protected])([email protected]6.0)':
|
||||
dependencies:
|
||||
'@types/leaflet': 1.9.22
|
||||
leaflet: 1.9.4
|
||||
maplibre-gl: 6.15.0
|
||||
maplibre-gl: 6.16.0
|
||||
|
||||
'@maplibre/[email protected]':
|
||||
dependencies:
|
||||
@@ -9803,7 +9803,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@mapbox/point-geometry': 1.1.0
|
||||
|
||||
'@maplibre/[email protected].2':
|
||||
'@maplibre/[email protected].3':
|
||||
dependencies:
|
||||
'@mapbox/point-geometry': 1.1.0
|
||||
'@types/geojson': 7946.0.16
|
||||
@@ -10675,14 +10675,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@swc/helpers': 0.5.23
|
||||
|
||||
'@rspack/[email protected].3(@rspack/[email protected](@swc/[email protected]))':
|
||||
'@rspack/[email protected].6(@rspack/[email protected](@swc/[email protected]))':
|
||||
optionalDependencies:
|
||||
'@rspack/core': 2.2.9(@swc/[email protected])
|
||||
|
||||
'@rspack/[email protected].1(@rspack/[email protected](@swc/[email protected]))':
|
||||
'@rspack/[email protected].2(@rspack/[email protected](@swc/[email protected]))':
|
||||
dependencies:
|
||||
'@rspack/core': 2.2.9(@swc/[email protected])
|
||||
'@rspack/dev-middleware': 2.0.3(@rspack/[email protected](@swc/[email protected]))
|
||||
'@rspack/dev-middleware': 2.0.6(@rspack/[email protected](@swc/[email protected]))
|
||||
|
||||
'@rspack/[email protected]': {}
|
||||
|
||||
@@ -10844,88 +10844,88 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
string.prototype.matchall: 4.0.12
|
||||
|
||||
'@tsparticles/animation-utils@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/animation-utils@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/basic@4.4.0':
|
||||
'@tsparticles/basic@4.5.0':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/plugin-blend': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/plugin-hex-color': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/plugin-hsl-color': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/plugin-move': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/plugin-rgb-color': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/shape-circle': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/updater-opacity': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/updater-out-modes': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/updater-paint': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/updater-size': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/engine': 4.5.0
|
||||
'@tsparticles/plugin-blend': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/plugin-hex-color': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/plugin-hsl-color': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/plugin-move': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/plugin-rgb-color': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/shape-circle': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/updater-opacity': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/updater-out-modes': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/updater-paint': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/updater-size': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
|
||||
'@tsparticles/canvas-utils@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/canvas-utils@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/engine@4.4.0': {}
|
||||
'@tsparticles/engine@4.5.0': {}
|
||||
|
||||
'@tsparticles/interaction-particles-links@4.4.0(@tsparticles/engine@4.4.0)(@tsparticles/plugin-interactivity@4.4.0(@tsparticles/engine@4.4.0))':
|
||||
'@tsparticles/interaction-particles-links@4.5.0(@tsparticles/engine@4.5.0)(@tsparticles/plugin-interactivity@4.5.0(@tsparticles/engine@4.5.0))':
|
||||
dependencies:
|
||||
'@tsparticles/canvas-utils': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/plugin-interactivity': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/canvas-utils': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/engine': 4.5.0
|
||||
'@tsparticles/plugin-interactivity': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
|
||||
'@tsparticles/plugin-blend@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-blend@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-hex-color@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-hex-color@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-hsl-color@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-hsl-color@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-interactivity@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-interactivity@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-move@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-move@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/plugin-rgb-color@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/plugin-rgb-color@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/preset-links@4.4.0':
|
||||
'@tsparticles/preset-links@4.5.0':
|
||||
dependencies:
|
||||
'@tsparticles/basic': 4.4.0
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/interaction-particles-links': 4.4.0(@tsparticles/engine@4.4.0)(@tsparticles/plugin-interactivity@4.4.0(@tsparticles/engine@4.4.0))
|
||||
'@tsparticles/plugin-interactivity': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/basic': 4.5.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
'@tsparticles/interaction-particles-links': 4.5.0(@tsparticles/engine@4.5.0)(@tsparticles/plugin-interactivity@4.5.0(@tsparticles/engine@4.5.0))
|
||||
'@tsparticles/plugin-interactivity': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
|
||||
'@tsparticles/shape-circle@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/shape-circle@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-opacity@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/updater-opacity@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/animation-utils': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/animation-utils': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-out-modes@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/updater-out-modes@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-paint@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/updater-paint@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tsparticles/updater-size@4.4.0(@tsparticles/engine@4.4.0)':
|
||||
'@tsparticles/updater-size@4.5.0(@tsparticles/engine@4.5.0)':
|
||||
dependencies:
|
||||
'@tsparticles/animation-utils': 4.4.0(@tsparticles/engine@4.4.0)
|
||||
'@tsparticles/engine': 4.4.0
|
||||
'@tsparticles/animation-utils': 4.5.0(@tsparticles/engine@4.5.0)
|
||||
'@tsparticles/engine': 4.5.0
|
||||
|
||||
'@tufjs/[email protected]': {}
|
||||
|
||||
@@ -11279,11 +11279,11 @@ snapshots:
|
||||
'@unrs/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@versatiles/[email protected]([email protected]5.0)':
|
||||
'@versatiles/[email protected]([email protected]6.0)':
|
||||
dependencies:
|
||||
'@maplibre/maplibre-gl-style-spec': 26.4.4
|
||||
optionalDependencies:
|
||||
maplibre-gl: 6.15.0
|
||||
maplibre-gl: 6.16.0
|
||||
|
||||
'@vibrant/[email protected]': {}
|
||||
|
||||
@@ -12190,14 +12190,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
wcwidth: 1.0.1
|
||||
|
||||
[email protected]([email protected](patch_hash=b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0))([email protected]):
|
||||
[email protected]([email protected](patch_hash=4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5))([email protected]):
|
||||
dependencies:
|
||||
chart2music: 1.20.0([email protected])
|
||||
echarts: 6.1.0(patch_hash=b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0)
|
||||
echarts: 6.1.0(patch_hash=4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
[email protected](patch_hash=b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0):
|
||||
[email protected](patch_hash=4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5):
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
zrender: 6.1.0
|
||||
@@ -12962,7 +12962,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
home-assistant-js-websocket@9.7.1: {}
|
||||
home-assistant-js-websocket@9.8.1: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -13666,7 +13666,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]5.0:
|
||||
[email protected]6.0:
|
||||
dependencies:
|
||||
'@mapbox/point-geometry': 1.1.0
|
||||
'@mapbox/tiny-sdf': 2.3.0
|
||||
@@ -13675,7 +13675,7 @@ snapshots:
|
||||
'@maplibre/geojson-vt': 6.3.0
|
||||
'@maplibre/maplibre-gl-style-spec': 26.4.4
|
||||
'@maplibre/mlt': 1.3.0
|
||||
'@maplibre/vt-pbf': 4.3.2
|
||||
'@maplibre/vt-pbf': 4.3.3
|
||||
'@types/geojson': 7946.0.16
|
||||
bidi-js: 1.1.0
|
||||
earcut: 3.2.4
|
||||
@@ -13689,7 +13689,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].1: {}
|
||||
[email protected].2: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -15770,7 +15770,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]6:
|
||||
[email protected]7:
|
||||
dependencies:
|
||||
commander: 2.20.3
|
||||
cssfilter: 0.0.10
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260930.0"
|
||||
version = "20261028.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -4,8 +4,8 @@ import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-icon-next";
|
||||
import "../components/ha-list";
|
||||
import "../components/ha-list-item";
|
||||
import "../components/item/ha-list-item-button";
|
||||
import "../components/list/ha-list-base";
|
||||
import type { AuthProvider } from "../data/auth";
|
||||
|
||||
declare global {
|
||||
@@ -32,21 +32,19 @@ export class HaPickAuthProvider extends LitElement {
|
||||
>${this._localize("ui.panel.page-authorize.pick_auth_provider")}</span
|
||||
>
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-list-base>
|
||||
${this.authProviders.map(
|
||||
(provider) => html`
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
role="button"
|
||||
<ha-list-item-button
|
||||
.auth_provider=${provider}
|
||||
@click=${this._handlePick}
|
||||
>
|
||||
${provider.name}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
<span slot="headline">${provider.name}</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -80,9 +78,9 @@ export class HaPickAuthProvider extends LitElement {
|
||||
background: var(--card-background-color);
|
||||
padding: 0 15px;
|
||||
}
|
||||
ha-list {
|
||||
ha-list-base {
|
||||
margin: 16px -16px 0;
|
||||
--mdc-list-side-padding: 24px;
|
||||
--ha-row-item-padding-inline: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Return a copy of the list with the value in it, without adding duplicates
|
||||
* @param values - The current list
|
||||
* @param value - The value to add
|
||||
*/
|
||||
export const arrayWith = <T>(
|
||||
values: readonly T[] | undefined,
|
||||
value: T
|
||||
): T[] => (values?.includes(value) ? [...values] : [...(values ?? []), value]);
|
||||
|
||||
/**
|
||||
* Return a copy of the list without the value
|
||||
* @param values - The current list
|
||||
* @param value - The value to remove
|
||||
*/
|
||||
export const arrayWithout = <T>(
|
||||
values: readonly T[] | undefined,
|
||||
value: T
|
||||
): T[] => (values ?? []).filter((v) => v !== value);
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { VisibilityCondition } from "../../panels/lovelace/common/validate-condition";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
|
||||
// YAML dashboards can hold entries like `- ` (null) or a bare string.
|
||||
const isConditionObject = (c: unknown): c is VisibilityCondition =>
|
||||
typeof c === "object" && c !== null;
|
||||
|
||||
/**
|
||||
* Drop entries that are not condition objects, at any depth. Returns the
|
||||
* same array when every entry is valid.
|
||||
*/
|
||||
export const dropInvalidConditions = (
|
||||
conditions: VisibilityCondition[]
|
||||
): VisibilityCondition[] => {
|
||||
let changed = false;
|
||||
const valid: VisibilityCondition[] = [];
|
||||
for (const c of conditions) {
|
||||
if (!isConditionObject(c)) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if ("conditions" in c && c.conditions != null) {
|
||||
const children = ensureArray(c.conditions);
|
||||
const validChildren = dropInvalidConditions(children);
|
||||
if (validChildren !== children) {
|
||||
changed = true;
|
||||
valid.push({ ...c, conditions: validChildren } as VisibilityCondition);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
valid.push(c);
|
||||
}
|
||||
return changed ? valid : conditions;
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { ReactiveControllerHost } from "lit";
|
||||
import { ref } from "lit/directives/ref";
|
||||
|
||||
const MAX_ROWS = 2;
|
||||
|
||||
// How many items fit in maxRows of a wrapping row, leaving room for the
|
||||
// "more" item after them when not all of them fit.
|
||||
export const fitInRows = (
|
||||
widths: number[],
|
||||
moreWidth: number,
|
||||
rowWidth: number,
|
||||
gap: number,
|
||||
maxRows: number
|
||||
): number => {
|
||||
const fits = (end: number, width: number) =>
|
||||
end === 0 || end + gap + width <= rowWidth;
|
||||
let rows = 1;
|
||||
let end = 0;
|
||||
let fit = 0;
|
||||
for (const [index, width] of widths.entries()) {
|
||||
if (rows < maxRows || fits(end, moreWidth)) {
|
||||
fit = index;
|
||||
}
|
||||
if (fits(end, width)) {
|
||||
end += (end === 0 ? 0 : gap) + width;
|
||||
} else {
|
||||
rows++;
|
||||
end = width;
|
||||
}
|
||||
if (rows > maxRows) {
|
||||
return fit;
|
||||
}
|
||||
}
|
||||
return widths.length;
|
||||
};
|
||||
|
||||
// Fits the items of a wrapping legend list into two rows. The list keeps its
|
||||
// "more" item rendered with the legend-more class, and items past the fit
|
||||
// stay rendered out of the flow, so both can always be measured.
|
||||
export class LegendFitController {
|
||||
// Items to show while collapsed, undefined until the list is measured
|
||||
public fit?: number;
|
||||
|
||||
private _host: ReactiveControllerHost;
|
||||
|
||||
private _element?: Element;
|
||||
|
||||
private _more?: Element;
|
||||
|
||||
private _widths: number[] = [];
|
||||
|
||||
private _rowWidth?: number;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
constructor(host: ReactiveControllerHost) {
|
||||
this._host = host;
|
||||
}
|
||||
|
||||
public target() {
|
||||
return ref(this._setElement);
|
||||
}
|
||||
|
||||
// Fits again from the current widths, for when the items change
|
||||
public refit() {
|
||||
this._widths = [];
|
||||
this._fit();
|
||||
}
|
||||
|
||||
private _setElement = (element?: Element) => {
|
||||
if (element === this._element) {
|
||||
return;
|
||||
}
|
||||
this._resizeObserver?.disconnect();
|
||||
this._element = element;
|
||||
this._more = undefined;
|
||||
if (element) {
|
||||
this._resizeObserver ??= new ResizeObserver(() => this._fit());
|
||||
this._resizeObserver.observe(element);
|
||||
}
|
||||
};
|
||||
|
||||
private _fit() {
|
||||
const list = this._element;
|
||||
const rowWidth = list?.getBoundingClientRect().width;
|
||||
if (!list || !rowWidth) {
|
||||
return;
|
||||
}
|
||||
const children = Array.from(list.children);
|
||||
const more = children.find((child) =>
|
||||
child.classList.contains("legend-more")
|
||||
);
|
||||
// Switching between More and Less resizes it, not always the list
|
||||
if (more !== this._more) {
|
||||
if (this._more) {
|
||||
this._resizeObserver?.unobserve(this._more);
|
||||
}
|
||||
if (more) {
|
||||
this._resizeObserver?.observe(more);
|
||||
}
|
||||
this._more = more;
|
||||
}
|
||||
const widths = children
|
||||
.filter((child) => child !== more)
|
||||
.map((child) => child.getBoundingClientRect().width);
|
||||
// Values resize the items on every state update. Fitting each item at its
|
||||
// widest since the last resize keeps the more button from flickering.
|
||||
if (rowWidth !== this._rowWidth || widths.length !== this._widths.length) {
|
||||
this._rowWidth = rowWidth;
|
||||
this._widths = widths;
|
||||
} else {
|
||||
this._widths = widths.map((width, index) =>
|
||||
Math.max(width, this._widths[index])
|
||||
);
|
||||
}
|
||||
const fit = fitInRows(
|
||||
this._widths,
|
||||
more?.getBoundingClientRect().width ?? 0,
|
||||
rowWidth,
|
||||
parseFloat(getComputedStyle(list).columnGap) || 0,
|
||||
MAX_ROWS
|
||||
);
|
||||
if (fit !== this.fit) {
|
||||
this.fit = fit;
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,10 @@ export interface QueryParamsControllerOptions {
|
||||
* Called when the page of the host is navigated to while the host is
|
||||
* connected, like a second link to an open page or coming back to it. Read
|
||||
* the URL with `searchParams()` and apply it, then `write()` the state back.
|
||||
* Gets the `popstate` or `location-changed` event, or nothing when the host
|
||||
* is connected again.
|
||||
*/
|
||||
navigated: () => void;
|
||||
navigated: (ev?: Event) => void;
|
||||
/**
|
||||
* The path of the host's page, for a page that can first be connected on
|
||||
* a sub-path it sends on to itself, like an add link. Defaults to the path
|
||||
@@ -159,7 +161,7 @@ export class QueryParamsController<
|
||||
}
|
||||
// Leaving the page gets here too, before the router swaps the page
|
||||
if (this._isActive()) {
|
||||
this._options.navigated();
|
||||
this._options.navigated(ev);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ export const parseTimeString = (timeString: string, timezone: string): Date => {
|
||||
* @returns true if current time matches the condition
|
||||
*/
|
||||
export const checkTimeInRange = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "locale" | "config">,
|
||||
{ after, before, weekdays }: Omit<TimeCondition, "condition">
|
||||
): boolean => {
|
||||
const timezone =
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeCssColor } from "../color/compute-color";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { stateColorCss } from "./state_color";
|
||||
|
||||
export const ALERT_DOMAINS = [
|
||||
"alarm_control_panel",
|
||||
@@ -8,8 +10,6 @@ export const ALERT_DOMAINS = [
|
||||
"lock",
|
||||
];
|
||||
|
||||
export const DEFAULT_ALERT_COLOR = "red";
|
||||
|
||||
export const computeAlertActiveStates = (entityId: string): string[] => {
|
||||
switch (computeDomain(entityId)) {
|
||||
case "alarm_control_panel":
|
||||
@@ -25,3 +25,16 @@ export const computeAlertActiveStates = (entityId: string): string[] => {
|
||||
|
||||
export const isAlertActive = (stateObj: HassEntity): boolean =>
|
||||
computeAlertActiveStates(stateObj.entity_id).includes(stateObj.state);
|
||||
|
||||
export const computeAlertColor = (
|
||||
stateObj: HassEntity,
|
||||
color: string
|
||||
): string | undefined => {
|
||||
if (color === "none") {
|
||||
return undefined;
|
||||
}
|
||||
if (color === "state") {
|
||||
return stateColorCss(stateObj);
|
||||
}
|
||||
return computeCssColor(color);
|
||||
};
|
||||
@@ -3,7 +3,10 @@ import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant, HomeAssistantRegistries } from "../../types";
|
||||
import type {
|
||||
HomeAssistantInternationalization,
|
||||
HomeAssistantRegistries,
|
||||
} from "../../types";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
import { computeRTL } from "../util/compute_rtl";
|
||||
import { computeAreaName } from "./compute_area_name";
|
||||
@@ -211,16 +214,20 @@ export interface EntityPickerDisplay {
|
||||
secondary?: string;
|
||||
}
|
||||
|
||||
export interface EntityPickerDisplayOptions extends HomeAssistantRegistries {
|
||||
language: HomeAssistantInternationalization["language"];
|
||||
translationMetadata: HomeAssistantInternationalization["translationMetadata"];
|
||||
}
|
||||
|
||||
export const computeEntityPickerDisplay = (
|
||||
hass: HomeAssistantRegistries &
|
||||
Pick<HomeAssistant, "language" | "translationMetadata">,
|
||||
options: EntityPickerDisplayOptions,
|
||||
stateObj: HassEntity
|
||||
): EntityPickerDisplay => {
|
||||
const parts = computeEntityNameParts(stateObj, hass);
|
||||
const parts = computeEntityNameParts(stateObj, options);
|
||||
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
options.language,
|
||||
options.translationMetadata.translations
|
||||
);
|
||||
|
||||
const primary = parts.entity || parts.device || stateObj.entity_id;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { stringCompare } from "../string/compare";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
@@ -248,7 +247,8 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
};
|
||||
|
||||
export const getStatesDomain = (
|
||||
hass: HomeAssistant,
|
||||
states: HassEntities,
|
||||
language: string,
|
||||
domain: string,
|
||||
attribute?: string | undefined
|
||||
): string[] => {
|
||||
@@ -274,15 +274,13 @@ export const getStatesDomain = (
|
||||
case "person":
|
||||
if (!attribute) {
|
||||
result.push(
|
||||
...Object.entries(hass.states)
|
||||
...Object.entries(states)
|
||||
.filter(
|
||||
([entityId]) =>
|
||||
computeDomain(entityId) === "zone" && entityId !== "zone.home"
|
||||
)
|
||||
.map(([_entityId, stateObj]) => computeZoneTrackerState(stateObj))
|
||||
.sort((zone1, zone2) =>
|
||||
stringCompare(zone1, zone2, hass.locale.language)
|
||||
)
|
||||
.sort((zone1, zone2) => stringCompare(zone1, zone2, language))
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -370,23 +368,24 @@ export const getValueAttribute = (
|
||||
): string | undefined => DOMAIN_VALUE_ATTRIBUTES[domain]?.[optionsAttribute];
|
||||
|
||||
export const getStates = (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
states: HassEntities,
|
||||
language: string,
|
||||
stateObj: HassEntity,
|
||||
attribute: string | undefined = undefined
|
||||
): string[] => {
|
||||
const domain = computeStateDomain(state);
|
||||
const domain = computeStateDomain(stateObj);
|
||||
const result: string[] = [];
|
||||
|
||||
// Fixed values based on a domain
|
||||
result.push(...getStatesDomain(hass, domain, attribute));
|
||||
result.push(...getStatesDomain(states, language, domain, attribute));
|
||||
|
||||
// Dynamic values based on the entities
|
||||
const optionsAttribute = getOptionsAttribute(domain, attribute);
|
||||
if (optionsAttribute) {
|
||||
const options = state.attributes[optionsAttribute];
|
||||
const options = stateObj.attributes[optionsAttribute];
|
||||
// Sensors only expose their options when their device class is `enum`.
|
||||
const enumSensor =
|
||||
domain !== "sensor" || state.attributes.device_class === "enum";
|
||||
domain !== "sensor" || stateObj.attributes.device_class === "enum";
|
||||
if (enumSensor && Array.isArray(options)) {
|
||||
result.push(...options);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
||||
};
|
||||
|
||||
export const toggleGroupEntities = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callService">,
|
||||
states: HassEntity[]
|
||||
) => {
|
||||
if (!states.length) {
|
||||
|
||||
@@ -28,7 +28,7 @@ const RASTER_TILE_URL = `${MAP_TILES_PATH}/raster/{z}/{x}/{y}.png?token={token}`
|
||||
// policy strips again unless the tiles ask for it back.
|
||||
const DEMO_RASTER_TILE_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||
const OSM_ATTRIBUTION =
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
||||
'<a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>';
|
||||
|
||||
// Browsers keep about 16 live WebGL contexts and drop the oldest, which a
|
||||
// dashboard full of map cards hits. A transient loss is restored, hence a grace.
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { NumberFormat } from "../../data/translation";
|
||||
import { blankBeforeUnit } from "../translations/blank_before_unit";
|
||||
|
||||
/**
|
||||
* Returns true if the entity is considered numeric based on the attributes it has
|
||||
@@ -71,6 +72,27 @@ export const formatNumber = (
|
||||
.map((part) => part.value)
|
||||
.join("");
|
||||
|
||||
/**
|
||||
* Formats a number like `formatNumber` and appends the unit, separated by the spacing the unit and locale expect.
|
||||
*
|
||||
* @param num The number to format
|
||||
* @param unit The unit to append, nothing is appended when empty
|
||||
* @param localeOptions The user-selected language and formatting, from `hass.locale`
|
||||
* @param options Intl.NumberFormatOptions to use
|
||||
*/
|
||||
export const formatNumberWithUnit = (
|
||||
num: string | number,
|
||||
unit: string | null | undefined,
|
||||
localeOptions?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): string => {
|
||||
const formatted = formatNumber(num, localeOptions, options);
|
||||
if (!unit) {
|
||||
return formatted;
|
||||
}
|
||||
return `${formatted}${blankBeforeUnit(unit, localeOptions)}${unit}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of objects containing the formatted number in parts
|
||||
* Similar to Intl.NumberFormat.prototype.formatToParts()
|
||||
|
||||
@@ -11,7 +11,7 @@ export const blankBeforePercent = (
|
||||
case "fr":
|
||||
case "sk":
|
||||
case "sv":
|
||||
return " ";
|
||||
return "\u00A0";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@ export const blankBeforeUnit = (
|
||||
if (localeOptions && unit === "%") {
|
||||
return blankBeforePercent(localeOptions);
|
||||
}
|
||||
return " ";
|
||||
return "\u00A0";
|
||||
};
|
||||
@@ -36,7 +36,9 @@ export const timeCachePromiseFunc = async <T, H = HomeAssistant>(
|
||||
return result.result;
|
||||
}
|
||||
|
||||
anyHass[cacheKey] = undefined;
|
||||
if (anyHass[cacheKey] === result) {
|
||||
anyHass[cacheKey] = undefined;
|
||||
}
|
||||
return timeCachePromiseFunc(
|
||||
cacheKey,
|
||||
cacheTime,
|
||||
@@ -73,7 +75,9 @@ export const timeCachePromiseFunc = async <T, H = HomeAssistant>(
|
||||
},
|
||||
// On failure, clear cache right away
|
||||
() => {
|
||||
anyHass[cacheKey] = undefined;
|
||||
if (anyHass[cacheKey] === cachePromise) {
|
||||
anyHass[cacheKey] = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import { LegendFitController } from "../../common/controllers/legend-fit-controller";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { getAllGraphColors } from "../../common/color/colors";
|
||||
@@ -71,8 +72,6 @@ import { downSampleLineData } from "./down-sample";
|
||||
import { wrapLitTooltipFormatter } from "./lit-tooltip-formatter";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
const LEGEND_OVERFLOW_LIMIT = 10;
|
||||
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
|
||||
const DOUBLE_TAP_TIME = 300;
|
||||
// echarts' own default, restored when switching back from touch input
|
||||
const DEFAULT_TOOLTIP_TRIGGER_ON = "mousemove|click|mousewheel";
|
||||
@@ -90,6 +89,8 @@ export const DEFAULT_CHART_WIDTH = 500;
|
||||
// is short enough for a whole screenful; on a desktop that would cover the page.
|
||||
const VISIBILITY_ROOT_MARGIN_NARROW = "100%";
|
||||
const VISIBILITY_ROOT_MARGIN = "300px";
|
||||
|
||||
export type ZoomSliderMode = "auto" | "always" | "never";
|
||||
const DEFERRED_PROPS = [
|
||||
"options",
|
||||
"data",
|
||||
@@ -164,9 +165,10 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
@property({ attribute: "small-controls", type: Boolean })
|
||||
public smallControls?: boolean;
|
||||
|
||||
// For charts zoomed from outside, like the synced History charts
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
// "auto" shows the zoom slider once zoomed. "never" hides every zoom
|
||||
// control, for charts zoomed from outside like the synced History charts.
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
// For charts sized to their content, like timelines, which grow to fit the
|
||||
// zoom slider instead of giving up plot height
|
||||
@@ -292,6 +294,12 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
|
||||
private _legendItemCount?: number;
|
||||
|
||||
private _legendFit = new LegendFitController(this);
|
||||
|
||||
private _renderedLegendFit?: number;
|
||||
|
||||
private _legendItemsKey?: string;
|
||||
|
||||
// Zoom window in percent
|
||||
@state() private _zoomRange: [number, number] = [0, 100];
|
||||
|
||||
@@ -522,7 +530,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated() {
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
const room = this._showZoomSlider();
|
||||
if (this.chart && room !== this._zoomSliderRoom) {
|
||||
this._zoomSliderRoom = room;
|
||||
@@ -538,6 +546,24 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
this.chart.setOption({ grid: this._getGridOption() });
|
||||
}
|
||||
}
|
||||
if (this.chart && this._legendFit.fit !== this._renderedLegendFit) {
|
||||
// Within a fixed height the host does not resize when the legend does.
|
||||
this._shouldResizeChart = true;
|
||||
this._resizeChartIfNeeded();
|
||||
}
|
||||
this._renderedLegendFit = this._legendFit.fit;
|
||||
if (changedProps.has("options") || changedProps.has("data")) {
|
||||
// New values keep the widest widths, but other items are fitted afresh
|
||||
const legendItemsKey = JSON.stringify(
|
||||
this._getLegendItems()?.map((item) =>
|
||||
typeof item === "string" ? item : [item.id, item.name]
|
||||
)
|
||||
);
|
||||
if (legendItemsKey !== this._legendItemsKey) {
|
||||
this._legendItemsKey = legendItemsKey;
|
||||
this._legendFit.refit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -594,7 +620,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
>
|
||||
${
|
||||
(this._isZoomed || this._isGraphRoamed) &&
|
||||
!this.hideZoomControls &&
|
||||
this.zoomSlider !== "never" &&
|
||||
!showZoomSlider
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
@@ -651,20 +677,15 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
const overflowLimit = this._isMobileSize
|
||||
? LEGEND_OVERFLOW_LIMIT_MOBILE
|
||||
: LEGEND_OVERFLOW_LIMIT;
|
||||
const fit = this._legendFit.fit ?? items.length;
|
||||
return html`<div
|
||||
class=${classMap({
|
||||
"chart-legend": true,
|
||||
"multiple-items": items.length > 1,
|
||||
})}
|
||||
>
|
||||
<ul>
|
||||
<ul ${this._legendFit.target()}>
|
||||
${items.map((item, index) => {
|
||||
if (!this.expandLegend && index >= overflowLimit) {
|
||||
return nothing;
|
||||
}
|
||||
let itemStyle: Record<string, any> = {};
|
||||
let value = "";
|
||||
let noLabelClick = false;
|
||||
@@ -693,7 +714,10 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
@pointerleave=${this._legendPointerCancel}
|
||||
@pointercancel=${this._legendPointerCancel}
|
||||
@contextmenu=${this._legendContextMenu}
|
||||
class=${classMap({ hidden: this._hiddenDatasets.has(id) })}
|
||||
class=${classMap({
|
||||
hidden: this._hiddenDatasets.has(id),
|
||||
overflow: !this.expandLegend && index >= fit,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -729,8 +753,13 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
</li>`;
|
||||
})}
|
||||
${
|
||||
items.length > overflowLimit
|
||||
? html`<li>
|
||||
items.length
|
||||
? html`<li
|
||||
class=${classMap({
|
||||
"legend-more": true,
|
||||
overflow: fit >= items.length,
|
||||
})}
|
||||
>
|
||||
<ha-assist-chip
|
||||
@click=${this._toggleExpandedLegend}
|
||||
filled
|
||||
@@ -741,7 +770,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
)
|
||||
: `${this._i18n.localize(
|
||||
"ui.components.history_charts.expand_legend"
|
||||
)} (${items.length - overflowLimit})`
|
||||
)} (${items.length - fit})`
|
||||
}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -943,6 +972,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
if (zoomStart !== 0 || zoomEnd !== 100) {
|
||||
this.zoom(zoomStart, zoomEnd, true);
|
||||
}
|
||||
fireEvent(this, "chart-setup");
|
||||
} finally {
|
||||
this._loading = false;
|
||||
this._applyDeferredWork();
|
||||
@@ -1037,9 +1067,12 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
private _showZoomSlider() {
|
||||
const [start, end] = this._zoomRange;
|
||||
return (
|
||||
!this.hideZoomControls &&
|
||||
this.zoomSlider !== "never" &&
|
||||
this._hasZoomableXAxis() &&
|
||||
(this._zoomSliderHeld || start !== 0 || end !== 100)
|
||||
(this.zoomSlider === "always" ||
|
||||
this._zoomSliderHeld ||
|
||||
start !== 0 ||
|
||||
end !== 100)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1270,6 +1303,10 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
},
|
||||
bar: { itemStyle: { barBorderWidth: 1.5 } },
|
||||
graph: {
|
||||
itemStyle: {
|
||||
borderColor: style.getPropertyValue("--primary-text-color"),
|
||||
borderWidth: 0,
|
||||
},
|
||||
label: {
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
textBorderColor,
|
||||
@@ -2166,6 +2203,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.chart-legend ul {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
@@ -2188,6 +2226,10 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
.chart-legend.multiple-items li:has(.value) {
|
||||
max-width: 300px;
|
||||
}
|
||||
.chart-legend li.overflow {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
.chart-legend .hidden {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@@ -2287,5 +2329,6 @@ declare global {
|
||||
end: number;
|
||||
};
|
||||
"chart-sankeyroam": { zoom: number };
|
||||
"chart-setup": undefined;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,13 @@ export interface NetworkData {
|
||||
|
||||
const PHYSICS_DISABLE_THRESHOLD = 512;
|
||||
|
||||
const SELECTED_NODE_MAX_ZOOM = 2;
|
||||
|
||||
// Keeps neighbors clear of the search field, buttons and legend
|
||||
const SELECTED_NODE_FIT_MARGIN = 72;
|
||||
|
||||
const FOLLOW_ROAM = "follow-selected-node";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
||||
let GraphChart: typeof import("echarts/lib/chart/graph/install");
|
||||
|
||||
@@ -92,6 +99,12 @@ export class HaNetworkGraph extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public searchFilter = "";
|
||||
|
||||
/**
|
||||
* Id of a node to highlight. The view centers on it, zoomed so its neighbors
|
||||
* stay visible, and follows it until the user moves the view.
|
||||
*/
|
||||
@property({ attribute: false }) public selectedNode?: string;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state() private _highlightedNodes?: Set<string>;
|
||||
@@ -106,6 +119,10 @@ export class HaNetworkGraph extends LitElement {
|
||||
|
||||
private _nodePositions: Record<string, { x: number; y: number }> = {};
|
||||
|
||||
private _followSelectedNode = false;
|
||||
|
||||
private _followedChart?: EChartsType;
|
||||
|
||||
@query("ha-chart-base") private _baseChart?: HaChartBase;
|
||||
|
||||
constructor() {
|
||||
@@ -145,11 +162,13 @@ export class HaNetworkGraph extends LitElement {
|
||||
this._reducedMotion,
|
||||
this._showLabels,
|
||||
isMobile,
|
||||
hasHighlightedNodes
|
||||
hasHighlightedNodes,
|
||||
this.selectedNode
|
||||
)}
|
||||
.options=${this._createOptions(this.data?.categories)}
|
||||
height="100%"
|
||||
.extraComponents=${[GraphChart]}
|
||||
@chart-setup=${this._updateFollowing}
|
||||
>
|
||||
<slot name="search" slot="search"></slot>
|
||||
<slot name="button" slot="button"></slot>
|
||||
@@ -212,6 +231,10 @@ export class HaNetworkGraph extends LitElement {
|
||||
this._applyHighlighting();
|
||||
this._updateMouseoverHandler();
|
||||
}
|
||||
if (changedProperties.has("selectedNode")) {
|
||||
this._followSelectedNode = !!this.selectedNode;
|
||||
this._updateFollowing();
|
||||
}
|
||||
}
|
||||
|
||||
private _nodeMatchesFilter(node: NetworkNode, lowerFilter: string): boolean {
|
||||
@@ -242,7 +265,8 @@ export class HaNetworkGraph extends LitElement {
|
||||
reducedMotion: boolean,
|
||||
showLabels: boolean,
|
||||
isMobile: boolean,
|
||||
hasHighlightedNodes?: boolean
|
||||
hasHighlightedNodes?: boolean,
|
||||
selectedNode?: string
|
||||
) => ({
|
||||
id: "network",
|
||||
type: "graph",
|
||||
@@ -285,7 +309,12 @@ export class HaNetworkGraph extends LitElement {
|
||||
},
|
||||
edgeSymbol: ["none", "arrow"],
|
||||
edgeSymbolSize: 10,
|
||||
data: this._getSeriesData(data.nodes, data.links, this._nodePositions),
|
||||
data: this._getSeriesData(
|
||||
data.nodes,
|
||||
data.links,
|
||||
this._nodePositions,
|
||||
selectedNode
|
||||
),
|
||||
links: data.links.map((link) => ({
|
||||
...link,
|
||||
value: link.reverseValue
|
||||
@@ -303,25 +332,28 @@ export class HaNetworkGraph extends LitElement {
|
||||
(
|
||||
nodes: NetworkNode[],
|
||||
links: NetworkLink[],
|
||||
nodePositions: Record<string, { x: number; y: number }>
|
||||
nodePositions: Record<string, { x: number; y: number }>,
|
||||
selectedNode?: string
|
||||
) =>
|
||||
this._getPositionedNodes(nodes, links, nodePositions).map(
|
||||
(node) =>
|
||||
({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
context: node.context,
|
||||
category: node.category,
|
||||
value: node.value,
|
||||
symbolSize: node.symbolSize || 30,
|
||||
symbol: node.symbol || "circle",
|
||||
itemStyle: node.itemStyle || {},
|
||||
label: node.label,
|
||||
fixed: node.fixed,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
}) as NonNullable<GraphSeriesOption["data"]>[number]
|
||||
),
|
||||
this._getPositionedNodes(nodes, links, nodePositions).map((node) => {
|
||||
const selected = node.id === selectedNode;
|
||||
return {
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
context: node.context,
|
||||
category: node.category,
|
||||
value: node.value,
|
||||
symbolSize: node.symbolSize || 30,
|
||||
symbol: node.symbol || "circle",
|
||||
itemStyle: selected
|
||||
? { ...node.itemStyle, borderWidth: 3 }
|
||||
: node.itemStyle || {},
|
||||
label: selected ? { ...node.label, fontWeight: "bold" } : node.label,
|
||||
fixed: node.fixed,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
} as NonNullable<GraphSeriesOption["data"]>[number];
|
||||
}),
|
||||
deepEqual
|
||||
);
|
||||
|
||||
@@ -477,6 +509,94 @@ export class HaNetworkGraph extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _updateFollowing() {
|
||||
const chart = this._followSelectedNode ? this._baseChart?.chart : undefined;
|
||||
if (chart !== this._followedChart) {
|
||||
const previous = this._followedChart;
|
||||
if (previous && !previous.isDisposed()) {
|
||||
previous.off("rendered", this._centerOnSelectedNode);
|
||||
previous.off("graphroam", this._handleGraphRoam);
|
||||
previous.getZr().off("drag", this._stopFollowing);
|
||||
}
|
||||
chart?.on("rendered", this._centerOnSelectedNode);
|
||||
chart?.on("graphroam", this._handleGraphRoam);
|
||||
chart?.getZr().on("drag", this._stopFollowing);
|
||||
this._followedChart = chart;
|
||||
}
|
||||
this._centerOnSelectedNode();
|
||||
}
|
||||
|
||||
private _stopFollowing = () => {
|
||||
this._followSelectedNode = false;
|
||||
this._updateFollowing();
|
||||
};
|
||||
|
||||
// A roam dispatched while ECharts renders is queued, so a flag set around
|
||||
// dispatchAction would be cleared before the event fires
|
||||
private _handleGraphRoam = (ev: unknown) => {
|
||||
if ((ev as { from?: string }).from !== FOLLOW_ROAM) {
|
||||
this._stopFollowing();
|
||||
}
|
||||
};
|
||||
|
||||
private _centerOnSelectedNode = () => {
|
||||
const chart = this._followedChart;
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
const series: any = chart
|
||||
// @ts-ignore private method but no other way to get the graph positions
|
||||
.getModel()
|
||||
.getSeriesByIndex(0);
|
||||
const node = series?.getGraph().getNodeById(this.selectedNode);
|
||||
const position: number[] | undefined = node?.getLayout();
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
const toPixel = (point: number[]) =>
|
||||
chart.convertToPixel({ seriesId: "network" }, point);
|
||||
const [x, y] = toPixel(position);
|
||||
const zoom = series.coordinateSystem.getRoamTransform()[0];
|
||||
const centerX = chart.getWidth() / 2;
|
||||
const centerY = chart.getHeight() / 2;
|
||||
// Zoom so the neighbors stay in view
|
||||
const fitX = Math.max(centerX - SELECTED_NODE_FIT_MARGIN, centerX / 2);
|
||||
const fitY = Math.max(centerY - SELECTED_NODE_FIT_MARGIN, centerY / 2);
|
||||
let targetZoom = SELECTED_NODE_MAX_ZOOM;
|
||||
for (const edge of node.edges) {
|
||||
const neighbor: number[] | undefined = (
|
||||
edge.node1 === node ? edge.node2 : edge.node1
|
||||
).getLayout();
|
||||
if (neighbor) {
|
||||
const [neighborX, neighborY] = toPixel(neighbor);
|
||||
targetZoom = Math.min(
|
||||
targetZoom,
|
||||
(zoom * fitX) / Math.abs(neighborX - x),
|
||||
(zoom * fitY) / Math.abs(neighborY - y)
|
||||
);
|
||||
}
|
||||
}
|
||||
const zoomChanged = Math.abs(zoom - targetZoom) >= 1e-3;
|
||||
if (
|
||||
!zoomChanged &&
|
||||
Math.abs(centerX - x) < 0.5 &&
|
||||
Math.abs(centerY - y) < 0.5
|
||||
) {
|
||||
return;
|
||||
}
|
||||
chart.dispatchAction({
|
||||
type: "graphRoam",
|
||||
from: FOLLOW_ROAM,
|
||||
seriesId: "network",
|
||||
dx: centerX - x,
|
||||
dy: centerY - y,
|
||||
// Any zoom, even 1, rescales every node and edge
|
||||
zoom: zoomChanged ? targetZoom / zoom : undefined,
|
||||
originX: centerX,
|
||||
originY: centerY,
|
||||
});
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._emphasisGuardHandler) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import type { VisualMapComponentOption } from "echarts/components";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import type { LineChartEntity, LineChartState } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { computeYAxisFractionDigits } from "./y-axis-fraction-digits";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
@@ -19,7 +20,8 @@ export const CLIMATE_MODE_CONFIGS = [
|
||||
] as const;
|
||||
|
||||
export interface StateHistoryChartLineDataParams {
|
||||
hass: HomeAssistant;
|
||||
localize: LocalizeFunc;
|
||||
states: HassEntities;
|
||||
data: LineChartEntity[];
|
||||
endTime: Date;
|
||||
names?: Record<string, string>;
|
||||
@@ -40,13 +42,13 @@ export interface StateHistoryChartLineData {
|
||||
/**
|
||||
* Transforms processed history (`LineChartEntity[]`) into ECharts series for
|
||||
* `state-history-chart-line`. Pure data processing: all environment inputs
|
||||
* (current time, theme style, hass) are injected so the transform is
|
||||
* (current time, theme style, localize, states) are injected so the transform is
|
||||
* deterministic and benchmarkable.
|
||||
*/
|
||||
export function generateStateHistoryChartLineData(
|
||||
params: StateHistoryChartLineDataParams
|
||||
): StateHistoryChartLineData | undefined {
|
||||
const { hass, computedStyles, endTime } = params;
|
||||
const { localize, states: currentStates, computedStyles, endTime } = params;
|
||||
// Work with numeric epoch timestamps (ms) instead of Date objects below.
|
||||
// Charts can hold a huge number of points, and allocating a Date per point
|
||||
// is needless GC pressure; the "time" axis consumes numbers natively.
|
||||
@@ -175,10 +177,10 @@ export function generateStateHistoryChartLineData(
|
||||
addDataSet(
|
||||
states.entity_id + "-current_temperature",
|
||||
params.showNames
|
||||
? hass.localize("ui.card.climate.current_temperature", {
|
||||
? localize("ui.card.climate.current_temperature", {
|
||||
name: name,
|
||||
})
|
||||
: hass.localize(
|
||||
: localize(
|
||||
"component.climate.entity_component._.state_attributes.current_temperature.name"
|
||||
)
|
||||
);
|
||||
@@ -186,10 +188,10 @@ export function generateStateHistoryChartLineData(
|
||||
addDataSet(
|
||||
`${states.entity_id}-${action}`,
|
||||
params.showNames
|
||||
? hass.localize(`ui.card.climate.${action}`, {
|
||||
? localize(`ui.card.climate.${action}`, {
|
||||
name: name,
|
||||
})
|
||||
: hass.localize(
|
||||
: localize(
|
||||
`component.climate.entity_component._.state_attributes.hvac_action.state.${action}`
|
||||
),
|
||||
computedStyles.getPropertyValue(cssVar),
|
||||
@@ -201,22 +203,22 @@ export function generateStateHistoryChartLineData(
|
||||
addDataSet(
|
||||
states.entity_id + "-target_temperature_mode",
|
||||
params.showNames
|
||||
? hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
? localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: hass.localize("ui.card.climate.high"),
|
||||
mode: localize("ui.card.climate.high"),
|
||||
})
|
||||
: hass.localize(
|
||||
: localize(
|
||||
"component.climate.entity_component._.state_attributes.target_temp_high.name"
|
||||
)
|
||||
);
|
||||
addDataSet(
|
||||
states.entity_id + "-target_temperature_mode_low",
|
||||
params.showNames
|
||||
? hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
? localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: hass.localize("ui.card.climate.low"),
|
||||
mode: localize("ui.card.climate.low"),
|
||||
})
|
||||
: hass.localize(
|
||||
: localize(
|
||||
"component.climate.entity_component._.state_attributes.target_temp_low.name"
|
||||
)
|
||||
);
|
||||
@@ -224,10 +226,10 @@ export function generateStateHistoryChartLineData(
|
||||
addDataSet(
|
||||
states.entity_id + "-target_temperature",
|
||||
params.showNames
|
||||
? hass.localize("ui.card.climate.target_temperature_entity", {
|
||||
? localize("ui.card.climate.target_temperature_entity", {
|
||||
name: name,
|
||||
})
|
||||
: hass.localize(
|
||||
: localize(
|
||||
"component.climate.entity_component._.state_attributes.temperature.name"
|
||||
)
|
||||
);
|
||||
@@ -281,10 +283,10 @@ export function generateStateHistoryChartLineData(
|
||||
addDataSet(
|
||||
states.entity_id + "-target_humidity",
|
||||
params.showNames
|
||||
? hass.localize("ui.card.humidifier.target_humidity_entity", {
|
||||
? localize("ui.card.humidifier.target_humidity_entity", {
|
||||
name: name,
|
||||
})
|
||||
: hass.localize(
|
||||
: localize(
|
||||
"component.humidifier.entity_component._.state_attributes.humidity.name"
|
||||
)
|
||||
);
|
||||
@@ -293,10 +295,10 @@ export function generateStateHistoryChartLineData(
|
||||
addDataSet(
|
||||
states.entity_id + "-current_humidity",
|
||||
params.showNames
|
||||
? hass.localize("ui.card.humidifier.current_humidity_entity", {
|
||||
? localize("ui.card.humidifier.current_humidity_entity", {
|
||||
name: name,
|
||||
})
|
||||
: hass.localize(
|
||||
: localize(
|
||||
"component.humidifier.entity_component._.state_attributes.current_humidity.name"
|
||||
)
|
||||
);
|
||||
@@ -308,10 +310,10 @@ export function generateStateHistoryChartLineData(
|
||||
addDataSet(
|
||||
states.entity_id + "-humidifying",
|
||||
params.showNames
|
||||
? hass.localize("ui.card.humidifier.humidifying", {
|
||||
? localize("ui.card.humidifier.humidifying", {
|
||||
name: name,
|
||||
})
|
||||
: hass.localize(
|
||||
: localize(
|
||||
"component.humidifier.entity_component._.state_attributes.action.state.humidifying"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
||||
@@ -321,10 +323,10 @@ export function generateStateHistoryChartLineData(
|
||||
addDataSet(
|
||||
states.entity_id + "-drying",
|
||||
params.showNames
|
||||
? hass.localize("ui.card.humidifier.drying", {
|
||||
? localize("ui.card.humidifier.drying", {
|
||||
name: name,
|
||||
})
|
||||
: hass.localize(
|
||||
: localize(
|
||||
"component.humidifier.entity_component._.state_attributes.action.state.drying"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
||||
@@ -334,10 +336,10 @@ export function generateStateHistoryChartLineData(
|
||||
addDataSet(
|
||||
states.entity_id + "-on",
|
||||
params.showNames
|
||||
? hass.localize("ui.card.humidifier.on_entity", {
|
||||
? localize("ui.card.humidifier.on_entity", {
|
||||
name: name,
|
||||
})
|
||||
: hass.localize("component.humidifier.entity_component._.state.on"),
|
||||
: localize("component.humidifier.entity_component._.state.on"),
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
@@ -445,7 +447,7 @@ export function generateStateHistoryChartLineData(
|
||||
// allow 1s of leeway for "now"
|
||||
const isUpToNow = nowMs - endTimeMs <= 1000;
|
||||
if (domain === "sensor" && isUpToNow && data.length === 1) {
|
||||
const stateObj = hass.states[states.entity_id];
|
||||
const stateObj = currentStates[states.entity_id];
|
||||
const currentValue = stateObj ? safeParseFloat(stateObj.state) : null;
|
||||
if (currentValue !== null) {
|
||||
data[0].data!.push([Math.max(nowMs, endTimeMs), currentValue]);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { HassConfig, HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -5,11 +6,25 @@ import type { VisualMapComponentOption } from "echarts/components";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import type { YAXisOption } from "echarts/types/dist/shared";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
|
||||
import {
|
||||
configContext,
|
||||
entitiesContext,
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import type { LineChartEntity } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { ZoomSliderMode } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import {
|
||||
@@ -22,11 +37,11 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
|
||||
import {
|
||||
getNumberFormatOptions,
|
||||
formatNumber,
|
||||
formatNumberWithUnit,
|
||||
} from "../../common/number/format_number";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import { computeAttributeValueDisplay } from "../../common/entity/compute_attribute_display";
|
||||
|
||||
// Minimum width reserved for the Y-axis labels; also the value _yWidth is
|
||||
@@ -50,8 +65,6 @@ const ENTITY_DATASET_SUFFIXES = [
|
||||
|
||||
@customElement("state-history-chart-line")
|
||||
export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data: LineChartEntity[] = [];
|
||||
|
||||
@property({ attribute: false }) public names?: Record<string, string>;
|
||||
@@ -93,8 +106,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
@state() private _chartData: LineSeriesOption[] = [];
|
||||
|
||||
@@ -114,6 +127,28 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
private _yAxisFractionDigits = 1;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _hassConfig!: HassConfig;
|
||||
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters!: HomeAssistantFormatters;
|
||||
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
private _entities!: HomeAssistant["entities"];
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states?: HassEntities;
|
||||
|
||||
private _optionsEntityStates: Record<string, string | undefined> = {};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
@@ -125,7 +160,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
.expandLegend=${this.expandLegend}
|
||||
.hideZoomControls=${this.hideZoomControls}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
.clickLabelForMoreInfo=${this.clickForMoreInfo}
|
||||
@legend-label-click=${this._handleLegendLabelClick}
|
||||
></ha-chart-base>
|
||||
@@ -136,8 +171,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
const time = params[0].axisValue;
|
||||
const title = formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
);
|
||||
const datapoints: Record<string, any>[] = [];
|
||||
// Index the hovered points by series so the per-dataset lookup below is
|
||||
@@ -178,22 +213,20 @@ export class StateHistoryChartLine extends LitElement {
|
||||
color: dataset.color,
|
||||
});
|
||||
});
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
|
||||
return html`${title}${datapoints.map((param) => {
|
||||
const entityId = this._entityIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const entry = this.hass.entities[entityId];
|
||||
const stateObj = this._states?.[entityId];
|
||||
const entry = this._entities?.[entityId];
|
||||
const stateValue = String(param.value[1]);
|
||||
const value = stateObj
|
||||
? this.hass.formatEntityState(stateObj, stateValue)
|
||||
: `${formatNumber(
|
||||
? this._formatters.formatEntityState(stateObj, stateValue)
|
||||
: formatNumberWithUnit(
|
||||
stateValue,
|
||||
this.hass.locale,
|
||||
this.unit,
|
||||
this._i18n.locale,
|
||||
getNumberFormatOptions(undefined, entry)
|
||||
)}${unit}`;
|
||||
);
|
||||
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
let statSuffix: TemplateResult | typeof nothing = nothing;
|
||||
@@ -201,8 +234,10 @@ export class StateHistoryChartLine extends LitElement {
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
param.value[0] < data.states[0].last_changed
|
||||
? this.hass.localize("ui.components.history_charts.source_stats")
|
||||
: this.hass.localize("ui.components.history_charts.source_history");
|
||||
? this._i18n.localize("ui.components.history_charts.source_stats")
|
||||
: this._i18n.localize(
|
||||
"ui.components.history_charts.source_history"
|
||||
);
|
||||
// Five non-breaking spaces indent the source label.
|
||||
statSuffix = html`<br />${"\u00a0".repeat(5)}${source}`;
|
||||
}
|
||||
@@ -243,7 +278,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
) {
|
||||
const id = ev.detail.id;
|
||||
let entityId = id;
|
||||
if (!this.hass.states[entityId]) {
|
||||
if (!this._states?.[entityId]) {
|
||||
for (const suffix of ENTITY_DATASET_SUFFIXES) {
|
||||
if (id.endsWith(suffix)) {
|
||||
entityId = id.slice(0, -suffix.length);
|
||||
@@ -251,7 +286,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.hass.states[entityId]) {
|
||||
if (this._states?.[entityId]) {
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
}
|
||||
@@ -282,12 +317,19 @@ export class StateHistoryChartLine extends LitElement {
|
||||
changedProps.has("paddingYAxis") ||
|
||||
changedProps.has("_visualMap") ||
|
||||
changedProps.has("_yWidth") ||
|
||||
(changedProps.has("hass") &&
|
||||
this._hasEntityStatesChanged(changedProps.get("hass")))
|
||||
changedProps.has("_i18n") ||
|
||||
changedProps.has("_hassConfig") ||
|
||||
this._hasEntityStatesChanged()
|
||||
) {
|
||||
this._optionsEntityStates = Object.fromEntries(
|
||||
this._entityIds.map((entityId) => [
|
||||
entityId,
|
||||
this._states?.[entityId]?.state,
|
||||
])
|
||||
);
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
);
|
||||
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||
this.minYAxis;
|
||||
@@ -358,7 +400,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
.map((d, i) => ({ dataset: d, entityId: this._entityIds[i] }))
|
||||
.filter((item) => !(item.dataset as LineSeriesOption).areaStyle)
|
||||
.map((item) => {
|
||||
const stateObj = this.hass.states[item.entityId];
|
||||
const stateObj = this._states?.[item.entityId];
|
||||
let value: string | undefined;
|
||||
|
||||
if (stateObj) {
|
||||
@@ -384,18 +426,18 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
// Use the helper to format temperature with proper unit
|
||||
value = computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
this._i18n.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
this._i18n.locale,
|
||||
this._hassConfig,
|
||||
this._entities,
|
||||
attribute
|
||||
);
|
||||
}
|
||||
|
||||
// Default for non-temperature datasets / missing attribute
|
||||
if (value === undefined) {
|
||||
value = this.hass.formatEntityState(stateObj);
|
||||
value = this._formatters.formatEntityState(stateObj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,10 +466,10 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _hasEntityStatesChanged(oldHass: HomeAssistant): boolean {
|
||||
private _hasEntityStatesChanged(): boolean {
|
||||
return this._entityIds.some(
|
||||
(entityId) =>
|
||||
this.hass.states[entityId]?.state !== oldHass.states[entityId]?.state
|
||||
this._states?.[entityId]?.state !== this._optionsEntityStates[entityId]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -439,7 +481,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
this._chartTime = new Date();
|
||||
|
||||
const data = generateStateHistoryChartLineData({
|
||||
hass: this.hass,
|
||||
localize: this._i18n.localize,
|
||||
states: this._states ?? {},
|
||||
data: this.data,
|
||||
endTime: this.endTime,
|
||||
names: this.names,
|
||||
@@ -461,7 +504,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
|
||||
private _formatYAxisLabel = (value: number) => {
|
||||
const label = formatNumber(value, this.hass.locale, {
|
||||
const label = formatNumber(value, this._i18n.locale, {
|
||||
minimumFractionDigits: value === 0 ? 0 : this._yAxisFractionDigits,
|
||||
maximumFractionDigits: this._yAxisFractionDigits,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -8,10 +9,20 @@ import type {
|
||||
} from "echarts/types/dist/shared";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import {
|
||||
configContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import type { TimelineEntity } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type {
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import { DEFAULT_CHART_WIDTH, MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { ZoomSliderMode } from "./ha-chart-base";
|
||||
import { itemTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import type { HaECOption, HaECSeries } from "../../resources/echarts/echarts";
|
||||
@@ -27,8 +38,6 @@ const GRID_BOTTOM = 30;
|
||||
|
||||
@customElement("state-history-chart-timeline")
|
||||
export class StateHistoryChartTimeline extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data: TimelineEntity[] = [];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -60,8 +69,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public chartIndex?;
|
||||
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
@state() private _chartData: CustomSeriesOption[] = [];
|
||||
|
||||
@@ -71,6 +80,16 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig!: HomeAssistantConfig;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states?: HassEntities;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
@@ -87,7 +106,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
grow-for-zoom-slider
|
||||
@chart-click=${this._handleChartClick}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
.hideZoomControls=${this.hideZoomControls}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
@@ -151,13 +170,13 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
? params[0]
|
||||
: params;
|
||||
const durationInMs = value![2] - value![1];
|
||||
const formattedDuration = `${this.hass.localize(
|
||||
const formattedDuration = `${this._i18n.localize(
|
||||
"ui.components.history_charts.duration"
|
||||
)}: ${millisecondsToDuration(durationInMs)}`;
|
||||
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
);
|
||||
return html`${
|
||||
seriesName
|
||||
@@ -169,12 +188,12 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
></ha-chart-tooltip-marker
|
||||
>${name}<br />${formatDateTimeWithSeconds(
|
||||
new Date(value![1]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
this._i18n.locale,
|
||||
this._hassConfig.config
|
||||
)}<br />${formatDateTimeWithSeconds(
|
||||
new Date(value![2]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
this._i18n.locale,
|
||||
this._hassConfig.config
|
||||
)}<br />${formattedDuration}`;
|
||||
};
|
||||
|
||||
@@ -199,7 +218,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
changedProps.has("showNames") ||
|
||||
changedProps.has("insideLabels") ||
|
||||
changedProps.has("paddingYAxis") ||
|
||||
changedProps.has("_yWidth")
|
||||
changedProps.has("_yWidth") ||
|
||||
changedProps.has("_i18n")
|
||||
) {
|
||||
this._createOptions();
|
||||
}
|
||||
@@ -216,8 +236,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
: this.paddingYAxis;
|
||||
const labelMargin = 5;
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
);
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
@@ -322,7 +342,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
private _generateData() {
|
||||
this._chartTime = new Date();
|
||||
this._chartData = generateStateHistoryChartTimelineData({
|
||||
states: this.hass.states,
|
||||
states: this._states ?? {},
|
||||
data: this.data,
|
||||
startTime: this.startTime,
|
||||
endTime: this.endTime,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import {
|
||||
@@ -9,19 +10,24 @@ import {
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { configContext } from "../../data/context";
|
||||
import type {
|
||||
HistoryResult,
|
||||
LineChartUnit,
|
||||
TimelineEntity,
|
||||
} from "../../data/history";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistantConfig } from "../../types";
|
||||
import "./state-history-chart-line";
|
||||
import type { StateHistoryChartLine } from "./state-history-chart-line";
|
||||
import "./state-history-chart-timeline";
|
||||
@@ -53,8 +59,6 @@ declare global {
|
||||
|
||||
@customElement("state-history-charts")
|
||||
export class StateHistoryCharts extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public historyData?: HistoryResult;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -116,6 +120,15 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@state() private _chartCount = 0;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _hassConfig!: HassConfig;
|
||||
|
||||
@queryAll("state-history-chart-line, state-history-chart-timeline")
|
||||
private _chartComponents!: NodeListOf<
|
||||
StateHistoryChartLine | StateHistoryChartTimeline
|
||||
@@ -127,21 +140,21 @@ export class StateHistoryCharts extends LitElement {
|
||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||
|
||||
protected render() {
|
||||
if (!isComponentLoaded(this.hass.config, "history")) {
|
||||
if (!isComponentLoaded(this._hassConfig, "history")) {
|
||||
return html`<div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.history_disabled")}
|
||||
${this._localize("ui.components.history_charts.history_disabled")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (this.isLoadingData && !this.historyData) {
|
||||
return html`<div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.loading_history")}
|
||||
${this._localize("ui.components.history_charts.loading_history")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (this._isHistoryEmpty()) {
|
||||
return html`<div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.no_history_found")}
|
||||
${this._localize("ui.components.history_charts.no_history_found")}
|
||||
</div>`;
|
||||
}
|
||||
const combinedItems = this.historyData!.timeline.length
|
||||
@@ -186,7 +199,6 @@ export class StateHistoryCharts extends LitElement {
|
||||
if (!Array.isArray(item)) {
|
||||
return html`<div class="entry-container line">
|
||||
<state-history-chart-line
|
||||
.hass=${this.hass}
|
||||
.unit=${item.unit}
|
||||
.data=${item.data}
|
||||
.identifier=${item.identifier}
|
||||
@@ -206,13 +218,12 @@ export class StateHistoryCharts extends LitElement {
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
.height=${this.virtualize ? undefined : this.height}
|
||||
.expandLegend=${this.expandLegend}
|
||||
?hide-zoom-controls=${this.syncCharts}
|
||||
.zoomSlider=${this.syncCharts ? "never" : "auto"}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
return html`<div class="entry-container timeline">
|
||||
<state-history-chart-timeline
|
||||
.hass=${this.hass}
|
||||
.data=${item}
|
||||
.startTime=${this._computedStartTime}
|
||||
.endTime=${this._computedEndTime}
|
||||
@@ -226,15 +237,12 @@ export class StateHistoryCharts extends LitElement {
|
||||
.clickForMoreInfo=${this.clickForMoreInfo}
|
||||
@y-width-changed=${this._yWidthChanged}
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
?hide-zoom-controls=${this.syncCharts}
|
||||
.zoomSlider=${this.syncCharts ? "never" : "auto"}
|
||||
></state-history-chart-timeline>
|
||||
</div> `;
|
||||
};
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.size === 1 && changedProps.has("hass")) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
changedProps.size === 1 &&
|
||||
changedProps.has("_maxYWidth") &&
|
||||
|
||||
@@ -19,9 +19,9 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
|
||||
import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
formatNumberWithUnit,
|
||||
getNumberFormatOptions,
|
||||
} from "../../common/number/format_number";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import { bidiIsolate } from "../../common/bidi";
|
||||
import {
|
||||
apiContext,
|
||||
@@ -297,9 +297,6 @@ export class StatisticsChart extends LitElement {
|
||||
const rendered: Record<string, boolean> = {};
|
||||
const chartIsBar = this.chartType.startsWith("bar");
|
||||
const period = this.period;
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this._i18n.locale)}${this.unit}`
|
||||
: "";
|
||||
const rows: {
|
||||
time?: string;
|
||||
color: string;
|
||||
@@ -375,7 +372,12 @@ export class StatisticsChart extends LitElement {
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
const value = `${formatNumber(rawValue, this._i18n.locale, options)}${unit}`;
|
||||
const value = formatNumberWithUnit(
|
||||
rawValue,
|
||||
this.unit,
|
||||
this._i18n.locale,
|
||||
options
|
||||
);
|
||||
|
||||
rows.push({
|
||||
time: rows.length === 0 ? rawTime : undefined,
|
||||
|
||||
@@ -124,7 +124,28 @@ export interface DataTableRowData {
|
||||
|
||||
export type SortableColumnContainer = Record<string, ClonedDataTableColumnData>;
|
||||
|
||||
// Slot for the actions at the end of a group's header, they don't toggle the
|
||||
// group. Put `ha-icon-button`s with a label in it. They get the rows' icon
|
||||
// colour and size by default, which `--data-table-group-actions-color`,
|
||||
// `--data-table-group-actions-button-size` and
|
||||
// `--data-table-group-actions-icon-size` change for all of them; style a
|
||||
// button itself for its own colour or size.
|
||||
export const GROUP_ACTIONS_SLOT_PREFIX = "group-actions-";
|
||||
|
||||
export const groupActionsSlot = (group: string) =>
|
||||
`${GROUP_ACTIONS_SLOT_PREFIX}${group}`;
|
||||
|
||||
const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
|
||||
|
||||
// The group a row's value of the group column puts it in. Rows without a value
|
||||
// share a reserved group, sorted to the bottom.
|
||||
const dataTableGroupKey = (
|
||||
value: string | number | boolean | null | undefined
|
||||
) => {
|
||||
const key = String(value);
|
||||
|
||||
return key === "undefined" ? UNDEFINED_GROUP_KEY : key;
|
||||
};
|
||||
const AUTO_FOCUS_ALLOWED_ACTIVE_TAGS = ["BODY", "HTML", "HOME-ASSISTANT"];
|
||||
|
||||
// Default row height, used to fill the viewport with skeleton rows.
|
||||
@@ -209,6 +230,9 @@ export class HaDataTable extends LitElement {
|
||||
@query(".sticky-group-header .group-header")
|
||||
private _stickyGroupHeaderCell?: HTMLElement;
|
||||
|
||||
@query(".sticky-group-header .group-actions")
|
||||
private _stickyGroupActions?: HTMLElement;
|
||||
|
||||
// Group pinned to the top of the scroller while its own header is scrolled out of view
|
||||
@state() private _stickyGroup?: string;
|
||||
|
||||
@@ -486,6 +510,14 @@ export class HaDataTable extends LitElement {
|
||||
)
|
||||
: [];
|
||||
|
||||
// The pinned group header is the same row as its group's header
|
||||
const stickyGroupIndex =
|
||||
this._stickyGroup === undefined
|
||||
? -1
|
||||
: items.findIndex(
|
||||
(item) => item.append && item.groupHeader === this._stickyGroup
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="mdc-data-table">
|
||||
<slot name="header" @slotchange=${this._calcTableHeight}>
|
||||
@@ -726,25 +758,20 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="mdc-data-table__content scroller ha-scrollbar ${classMap(
|
||||
{ grouped: Boolean(this.groupColumn) && !this.autoHeight }
|
||||
)}"
|
||||
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
|
||||
@scroll=${this._saveScrollPos}
|
||||
@visibilityChanged=${this._handleVisibilityChanged}
|
||||
.items=${items}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${renderRow}
|
||||
></lit-virtualizer>
|
||||
${
|
||||
// Rendered before the rows, so the group's actions slot
|
||||
// shows here and not in its scrolled out header row
|
||||
this.groupColumn &&
|
||||
!this.autoHeight &&
|
||||
this._stickyGroup !== undefined
|
||||
? html`<div
|
||||
class="mdc-data-table__row sticky-group-header"
|
||||
aria-hidden="true"
|
||||
role="row"
|
||||
aria-rowindex=${ifDefined(
|
||||
stickyGroupIndex === -1
|
||||
? undefined
|
||||
: stickyGroupIndex + (this.loadError ? 3 : 2)
|
||||
)}
|
||||
.group=${this._stickyGroup}
|
||||
@click=${this._collapseStickyGroup}
|
||||
>
|
||||
@@ -757,6 +784,18 @@ export class HaDataTable extends LitElement {
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="mdc-data-table__content scroller ha-scrollbar ${classMap(
|
||||
{ grouped: Boolean(this.groupColumn) && !this.autoHeight }
|
||||
)}"
|
||||
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
|
||||
@scroll=${this._saveScrollPos}
|
||||
@visibilityChanged=${this._handleVisibilityChanged}
|
||||
.items=${items}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${renderRow}
|
||||
></lit-virtualizer>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
@@ -1031,12 +1070,10 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
if (groupColumn) {
|
||||
const isGroupSortColumn = sortColumn === groupColumn;
|
||||
const grouped = groupBy(items, (item) => item[groupColumn]);
|
||||
if (grouped.undefined) {
|
||||
// make sure ungrouped items are at the bottom
|
||||
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
|
||||
delete grouped.undefined;
|
||||
}
|
||||
|
||||
const grouped = groupBy(items, (item) =>
|
||||
dataTableGroupKey(item[groupColumn])
|
||||
);
|
||||
const sortedEntries: [string, DataTableRowData[]][] = Object.keys(
|
||||
grouped
|
||||
)
|
||||
@@ -1113,9 +1150,11 @@ export class HaDataTable extends LitElement {
|
||||
localize: LocalizeFunc | undefined,
|
||||
sticky = false
|
||||
) {
|
||||
// The sticky copy is hidden from assistive technology, so its button is
|
||||
// made inert. It handles clicks on its whole row instead, as its cell
|
||||
// shifts with horizontal scrolling.
|
||||
// The sticky copy's button and name are hidden from assistive technology,
|
||||
// and its button is made inert. It handles clicks on its whole row
|
||||
// instead, as its cell shifts with horizontal scrolling. The group's
|
||||
// actions show in the sticky copy while it is pinned, so they stay
|
||||
// available there.
|
||||
return html`<div
|
||||
class="mdc-data-table__cell group-header"
|
||||
role="cell"
|
||||
@@ -1133,14 +1172,27 @@ export class HaDataTable extends LitElement {
|
||||
?inert=${sticky}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${
|
||||
groupName === UNDEFINED_GROUP_KEY
|
||||
? localize?.("ui.components.data-table.ungrouped") || "Ungrouped"
|
||||
: groupName || ""
|
||||
}
|
||||
<span
|
||||
class="group-name"
|
||||
aria-hidden=${ifDefined(sticky ? "true" : undefined)}
|
||||
>
|
||||
${
|
||||
groupName === UNDEFINED_GROUP_KEY
|
||||
? localize?.("ui.components.data-table.ungrouped") || "Ungrouped"
|
||||
: groupName || ""
|
||||
}
|
||||
</span>
|
||||
<div class="group-actions" @click=${this._stopGroupToggle}>
|
||||
<slot name=${groupActionsSlot(groupName)}></slot>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Actions don't toggle the group their header belongs to
|
||||
private _stopGroupToggle(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _memFilterData = memoizeOne(
|
||||
(
|
||||
data: DataTableRowData[],
|
||||
@@ -1442,6 +1494,11 @@ export class HaDataTable extends LitElement {
|
||||
if (this._stickyGroupHeaderCell) {
|
||||
this._stickyGroupHeaderCell.style.transform = `translateX(${-scrollLeft}px)`;
|
||||
}
|
||||
|
||||
// Its actions stay at the end of the visible row, like the rows' own
|
||||
if (this._stickyGroupActions) {
|
||||
this._stickyGroupActions.style.transform = `translateX(${scrollLeft}px)`;
|
||||
}
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
@@ -1509,18 +1566,15 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
public collapseAllGroups() {
|
||||
if (
|
||||
!this.groupColumn ||
|
||||
!this.data.some((item) => item[this.groupColumn!])
|
||||
) {
|
||||
const groupColumn = this.groupColumn;
|
||||
|
||||
if (!groupColumn || !this.data.some((item) => item[groupColumn])) {
|
||||
return;
|
||||
}
|
||||
const grouped = groupBy(this.data, (item) => item[this.groupColumn!]);
|
||||
if (grouped.undefined) {
|
||||
// undefined is a reserved group name
|
||||
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
|
||||
delete grouped.undefined;
|
||||
}
|
||||
|
||||
const grouped = groupBy(this.data, (item) =>
|
||||
dataTableGroupKey(item[groupColumn])
|
||||
);
|
||||
this._collapsedGroups = Object.keys(grouped);
|
||||
this._lastSelectedRowId = null;
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
@@ -1879,6 +1933,16 @@ export class HaDataTable extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
/* Clipping would make the cell the actions' sticky scroll container,
|
||||
and cut off the pinned header's actions */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.group-header ha-icon-button {
|
||||
@@ -1889,6 +1953,36 @@ export class HaDataTable extends LitElement {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
/* Centre in the row, not below the header's top padding */
|
||||
align-self: stretch;
|
||||
margin-top: -12px;
|
||||
margin-inline-start: auto;
|
||||
/* The rows' icon button size, inherited by the slotted ones */
|
||||
--_button-size: var(--data-table-group-actions-button-size, 48px);
|
||||
--ha-icon-button-size: var(--_button-size);
|
||||
--mdc-icon-size: var(--data-table-group-actions-icon-size, 24px);
|
||||
/* Centre the last action over the row's last cell, such as an
|
||||
overflow menu, which is a 48px icon button */
|
||||
padding-inline-end: calc(
|
||||
var(--ha-space-4) + (48px - var(--_button-size)) / 2
|
||||
);
|
||||
/* Stay in view when the rows scroll horizontally */
|
||||
position: sticky;
|
||||
inset-inline-end: 0;
|
||||
cursor: default;
|
||||
/* Don't pass the group name's weight on to slotted menus */
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
/* Inherited by the slotted icon buttons, like the rows' icon cells */
|
||||
color: var(
|
||||
--data-table-group-actions-color,
|
||||
var(--secondary-text-color)
|
||||
);
|
||||
}
|
||||
|
||||
.sticky-group-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiCalendarToday } from "@mdi/js";
|
||||
import "cally";
|
||||
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
@@ -32,8 +32,8 @@ import type { HaBaseTimeInput } from "../ha-base-time-input";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-icon-button-next";
|
||||
import "../ha-icon-button-prev";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "../item/ha-list-item-button";
|
||||
import "../list/ha-list-base";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
import type { DateRangePickerRanges } from "./ha-date-range-picker";
|
||||
@@ -166,11 +166,19 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-list @action=${this._setDateRange} activatable>
|
||||
${Object.keys(this.ranges!).map(
|
||||
(name) => html`<ha-list-item>${name}</ha-list-item>`
|
||||
<ha-list-base>
|
||||
${Object.entries(this.ranges!).map(
|
||||
([name, range], index) => html`
|
||||
<ha-list-item-button
|
||||
.range=${range}
|
||||
.index=${index}
|
||||
@click=${this._setDateRange}
|
||||
>
|
||||
<span slot="headline">${name}</span>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -343,11 +351,12 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
this._saveDateRangePreset(chip.range, chip.index);
|
||||
}
|
||||
|
||||
private _setDateRange(ev: HASSDomEvent<ActionDetail>) {
|
||||
const dateRange: [Date, Date] = Object.values(this.ranges!)[
|
||||
ev.detail.index
|
||||
];
|
||||
this._saveDateRangePreset(dateRange, ev.detail.index);
|
||||
private _setDateRange(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLElement & { range: [Date, Date]; index: number }
|
||||
>
|
||||
) {
|
||||
this._saveDateRangePreset(ev.currentTarget.range, ev.currentTarget.index);
|
||||
}
|
||||
|
||||
private _saveDateRangePreset(range: [Date, Date], index: number) {
|
||||
|
||||
@@ -9,13 +9,13 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||
@customElement("ha-device-action-picker")
|
||||
class HaDeviceActionPicker extends HaDeviceAutomationPicker<DeviceAction> {
|
||||
protected get NO_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.actions.no_actions"
|
||||
);
|
||||
}
|
||||
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { preserveUnchangedEntityStatesRecord } from "../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import {
|
||||
apiContext,
|
||||
formattersContext,
|
||||
fullEntitiesContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import type { DeviceAutomation } from "../../data/device/device_automation";
|
||||
import {
|
||||
deviceAutomationsEqual,
|
||||
sortDeviceAutomations,
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { CallWS, HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import { entityRegistryById } from "../../data/entity/entity_registry";
|
||||
import type {
|
||||
CallWS,
|
||||
HomeAssistantFormatters,
|
||||
ValueChangedEvent,
|
||||
} from "../../types";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import {
|
||||
@@ -29,8 +43,6 @@ const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
export abstract class HaDeviceAutomationPicker<
|
||||
T extends DeviceAutomation,
|
||||
> extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public deviceId?: string;
|
||||
@@ -47,8 +59,49 @@ export abstract class HaDeviceAutomationPicker<
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg: EntityRegistryEntry[] = [];
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
protected _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@transform<HassEntities, HassEntities>({
|
||||
transformer: function (
|
||||
this: HaDeviceAutomationPicker<DeviceAutomation>,
|
||||
states
|
||||
) {
|
||||
const registryById = entityRegistryById(this._entityReg ?? []);
|
||||
const picked: HassEntities = {};
|
||||
[...(this._automations ?? []), this.value].forEach((automation) => {
|
||||
const id = automation?.entity_id;
|
||||
const entityId = id?.includes(".")
|
||||
? id
|
||||
: id && registryById[id]?.entity_id;
|
||||
if (entityId && states?.[entityId]) {
|
||||
picked[entityId] = states[entityId];
|
||||
}
|
||||
});
|
||||
return preserveUnchangedEntityStatesRecord(this._states, picked);
|
||||
},
|
||||
watch: ["_automations", "value", "_entityReg"],
|
||||
})
|
||||
private _states: HassEntities = {};
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
@transform<
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantFormatters["formatEntityName"]
|
||||
>({
|
||||
transformer: ({ formatEntityName }) => formatEntityName,
|
||||
})
|
||||
private _formatEntityName!: HomeAssistantFormatters["formatEntityName"];
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
protected get NO_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.actions.no_actions"
|
||||
);
|
||||
}
|
||||
@@ -56,7 +109,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
private _localizeDeviceAutomation: (
|
||||
localize: LocalizeFunc,
|
||||
states: HassEntities,
|
||||
formatEntityName: HomeAssistant["formatEntityName"],
|
||||
formatEntityName: HomeAssistantFormatters["formatEntityName"],
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
automation: T
|
||||
) => string;
|
||||
@@ -109,11 +162,19 @@ export abstract class HaDeviceAutomationPicker<
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
.disabled=${!this._automations || this._automations.length === 0}
|
||||
.getItems=${this._getItems(value, this._automations)}
|
||||
.getItems=${this._getItems(
|
||||
value,
|
||||
this._automations,
|
||||
this._i18n.localize,
|
||||
this._i18n.locale.language,
|
||||
this._states,
|
||||
this._formatEntityName,
|
||||
this._entityReg
|
||||
)}
|
||||
@value-changed=${this._automationChanged}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
.unknownItemText=${this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
)}
|
||||
hide-clear-icon
|
||||
@@ -130,17 +191,25 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(value: string, automations: T[] | undefined) => {
|
||||
(
|
||||
value: string,
|
||||
automations: T[] | undefined,
|
||||
localize: LocalizeFunc,
|
||||
language: string,
|
||||
states: HassEntities,
|
||||
formatEntityName: HomeAssistantFormatters["formatEntityName"],
|
||||
entityReg: EntityRegistryEntry[]
|
||||
) => {
|
||||
if (!automations) {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
const automationListItems = automations.map((automation, idx) => {
|
||||
const primary = this._localizeDeviceAutomation(
|
||||
this.hass.localize,
|
||||
this.hass.states,
|
||||
this.hass.formatEntityName,
|
||||
this._entityReg,
|
||||
localize,
|
||||
states,
|
||||
formatEntityName,
|
||||
entityReg,
|
||||
automation
|
||||
);
|
||||
return {
|
||||
@@ -150,11 +219,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
});
|
||||
|
||||
automationListItems.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.primary,
|
||||
b.primary,
|
||||
this.hass.locale.language
|
||||
)
|
||||
caseInsensitiveStringCompare(a.primary, b.primary, language)
|
||||
);
|
||||
|
||||
if (value === NO_AUTOMATION_KEY) {
|
||||
@@ -185,9 +250,9 @@ export abstract class HaDeviceAutomationPicker<
|
||||
|
||||
const text = described
|
||||
? this._localizeDeviceAutomation(
|
||||
this.hass.localize,
|
||||
this.hass.states,
|
||||
this.hass.formatEntityName,
|
||||
this._i18n.localize,
|
||||
this._states,
|
||||
this._formatEntityName,
|
||||
this._entityReg,
|
||||
described
|
||||
)
|
||||
@@ -204,7 +269,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this._automations = this.deviceId
|
||||
? (
|
||||
await this._fetchDeviceAutomations(
|
||||
this.hass.callWS,
|
||||
this._api.callWS,
|
||||
this.deviceId
|
||||
).catch(() => [] as T[])
|
||||
).sort(sortDeviceAutomations)
|
||||
|
||||
@@ -9,13 +9,13 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||
@customElement("ha-device-condition-picker")
|
||||
class HaDeviceConditionPicker extends HaDeviceAutomationPicker<DeviceCondition> {
|
||||
protected get NO_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.conditions.no_conditions"
|
||||
);
|
||||
}
|
||||
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.conditions.unknown_condition"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,15 +2,28 @@ import { mdiSwapHorizontal } from "@mdi/js";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { getDeviceArea } from "../../common/entity/context/get_device_context";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
||||
import {
|
||||
apiContext,
|
||||
brandsAccessTokenContext,
|
||||
configContext,
|
||||
connectionContext,
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
statesContext,
|
||||
uiContext,
|
||||
} from "../../data/context";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
getDevices,
|
||||
@@ -23,7 +36,11 @@ import {
|
||||
} from "../../data/device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantUI,
|
||||
} from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import "../ha-alert";
|
||||
import "../ha-button";
|
||||
@@ -42,7 +59,40 @@ export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||
|
||||
@customElement("ha-device-picker")
|
||||
export class HaDevicePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries!: ContextType<typeof registriesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
@transform<HomeAssistantUI, boolean | undefined>({
|
||||
transformer: (ui) => ui?.themes?.darkMode,
|
||||
})
|
||||
private _darkMode?: boolean;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, string | undefined>({
|
||||
transformer: (config) => config?.auth?.data.hassUrl,
|
||||
})
|
||||
private _hassUrl?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: brandsAccessTokenContext, subscribe: true })
|
||||
private _brandsAccessToken?: string;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api?: ContextType<typeof apiContext>;
|
||||
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
@@ -123,7 +173,9 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
private _getDevicesMemoized = memoizeOne(
|
||||
(
|
||||
_devices: HomeAssistant["devices"],
|
||||
states: HomeAssistant["states"],
|
||||
registries: ContextType<typeof registriesContext>,
|
||||
i18n: ContextType<typeof internationalizationContext>,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
@@ -133,7 +185,15 @@ export class HaDevicePicker extends LitElement {
|
||||
excludeDevices?: string[],
|
||||
value?: string
|
||||
) =>
|
||||
getDevices(this.hass, configEntryLookup, {
|
||||
getDevices({
|
||||
states,
|
||||
areas: registries.areas,
|
||||
devices: registries.devices,
|
||||
entities: registries.entities,
|
||||
localize: i18n.localize,
|
||||
language: i18n.locale.language,
|
||||
translationMetadata: i18n.translationMetadata,
|
||||
configEntryLookup,
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
@@ -150,21 +210,24 @@ export class HaDevicePicker extends LitElement {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
const devices = this._registries?.devices;
|
||||
if (
|
||||
!this.hass ||
|
||||
!devices ||
|
||||
!this.value ||
|
||||
this._compositeSplits !== undefined ||
|
||||
this._loadingCompositeSplits
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const oldHass = changedProperties.get("hass") as HomeAssistant | undefined;
|
||||
const oldRegistries = changedProperties.get("_registries") as
|
||||
ContextType<typeof registriesContext> | undefined;
|
||||
const devicesChanged =
|
||||
changedProperties.has("hass") && this.hass.devices !== oldHass?.devices;
|
||||
changedProperties.has("_registries") &&
|
||||
devices !== oldRegistries?.devices;
|
||||
if (
|
||||
(changedProperties.has("value") || devicesChanged) &&
|
||||
!this.hass.devices[this.value]
|
||||
!devices[this.value]
|
||||
) {
|
||||
// The selected device is not in the registry; it might be a legacy
|
||||
// composite device that was split into separate devices. Fetch the
|
||||
@@ -174,16 +237,25 @@ export class HaDevicePicker extends LitElement {
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
if (!this._api) {
|
||||
return;
|
||||
}
|
||||
const configEntries = await getConfigEntries(this._api);
|
||||
this._configEntryLookup = Object.fromEntries(
|
||||
configEntries.map((entry) => [entry.entry_id, entry])
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadCompositeSplits() {
|
||||
if (!this._api || !this._connection) {
|
||||
return;
|
||||
}
|
||||
this._loadingCompositeSplits = true;
|
||||
try {
|
||||
this._compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
this._compositeSplits = await fetchDeviceCompositeSplits(
|
||||
this._connection.connection,
|
||||
this._api.callWS
|
||||
);
|
||||
} catch (_err) {
|
||||
this._compositeSplits = {};
|
||||
} finally {
|
||||
@@ -194,12 +266,12 @@ export class HaDevicePicker extends LitElement {
|
||||
private _getReplacement = memoizeOne(
|
||||
(
|
||||
value: string | undefined,
|
||||
_devices: HomeAssistant["devices"],
|
||||
devices: HomeAssistant["devices"],
|
||||
compositeSplits: DeviceCompositeSplits | undefined,
|
||||
items: (DevicePickerItem | string)[],
|
||||
replacementDeviceIds: string[] | undefined
|
||||
) => {
|
||||
if (!value || !compositeSplits || this.hass.devices[value]) {
|
||||
if (!value || !compositeSplits || devices[value]) {
|
||||
return undefined;
|
||||
}
|
||||
const split = compositeSplits[value];
|
||||
@@ -224,7 +296,9 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
private _getItems = () =>
|
||||
this._getDevicesMemoized(
|
||||
this.hass.devices,
|
||||
this._states,
|
||||
this._registries,
|
||||
this._i18n,
|
||||
this._configEntryLookup,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
@@ -264,10 +338,18 @@ export class HaDevicePicker extends LitElement {
|
||||
};
|
||||
|
||||
private _valueRenderer = memoizeOne(
|
||||
(configEntriesLookup: Record<string, ConfigEntry>, isReplaced: boolean) =>
|
||||
(
|
||||
configEntriesLookup: Record<string, ConfigEntry>,
|
||||
isReplaced: boolean,
|
||||
registries: ContextType<typeof registriesContext>,
|
||||
localize: ContextType<typeof internationalizationContext>["localize"],
|
||||
darkMode: boolean | undefined,
|
||||
hassUrl: string | undefined,
|
||||
_brandsAccessToken: string | undefined
|
||||
) =>
|
||||
(value: string) => {
|
||||
const deviceId = value;
|
||||
const device = this.hass.devices[deviceId];
|
||||
const device = registries.devices[deviceId];
|
||||
|
||||
if (!device) {
|
||||
// The removed device has no name left to show, so say what happened
|
||||
@@ -280,16 +362,18 @@ export class HaDevicePicker extends LitElement {
|
||||
.path=${mdiSwapHorizontal}
|
||||
></ha-svg-icon>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.components.device-picker.device_replaced"
|
||||
)}</span
|
||||
>${localize("ui.components.device-picker.device_replaced")}</span
|
||||
>
|
||||
`;
|
||||
}
|
||||
return html`<span slot="headline">${deviceId}</span>`;
|
||||
}
|
||||
|
||||
const area = getDeviceArea(device, this.hass.areas, this.hass.devices);
|
||||
const area = getDeviceArea(
|
||||
device,
|
||||
registries.areas,
|
||||
registries.devices
|
||||
);
|
||||
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
@@ -313,9 +397,9 @@ export class HaDevicePicker extends LitElement {
|
||||
{
|
||||
domain: configEntry.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
darkOptimized: darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
hassUrl
|
||||
)}
|
||||
/>`
|
||||
: nothing
|
||||
@@ -328,8 +412,8 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
private _rowRenderer: RenderItemFunction<DevicePickerItem> = (item) => {
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
);
|
||||
return html`
|
||||
<ha-combo-box-item
|
||||
@@ -368,9 +452,9 @@ export class HaDevicePicker extends LitElement {
|
||||
{
|
||||
domain: item.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes.darkMode,
|
||||
darkOptimized: this._darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
this._hassUrl
|
||||
)}
|
||||
/>
|
||||
`
|
||||
@@ -399,18 +483,18 @@ export class HaDevicePicker extends LitElement {
|
||||
protected render() {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.device-picker.placeholder");
|
||||
this._i18n.localize("ui.components.device-picker.placeholder");
|
||||
|
||||
// Only resolve a replacement (which needs the full item list) when the
|
||||
// value is a missing device that we know was replaced, to avoid computing
|
||||
// the item list on every render for the common case.
|
||||
const replacement =
|
||||
this.value &&
|
||||
!this.hass.devices[this.value] &&
|
||||
!this._registries.devices[this.value] &&
|
||||
this._compositeSplits?.[this.value]
|
||||
? this._getReplacement(
|
||||
this.value,
|
||||
this.hass.devices,
|
||||
this._registries.devices,
|
||||
this._compositeSplits,
|
||||
this._getItems(),
|
||||
this.replacementDeviceIds
|
||||
@@ -423,7 +507,12 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
const valueRenderer = this._valueRenderer(
|
||||
this._configEntryLookup,
|
||||
canReplace
|
||||
canReplace,
|
||||
this._registries,
|
||||
this._i18n.localize,
|
||||
this._darkMode,
|
||||
this._hassUrl,
|
||||
this._brandsAccessToken
|
||||
);
|
||||
|
||||
return html`
|
||||
@@ -435,7 +524,7 @@ export class HaDevicePicker extends LitElement {
|
||||
.label=${this.label}
|
||||
.searchLabel=${this.searchLabel}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
.emptyLabel=${this._i18n.localize(
|
||||
"ui.components.device-picker.no_devices"
|
||||
)}
|
||||
.placeholder=${placeholder}
|
||||
@@ -447,7 +536,7 @@ export class HaDevicePicker extends LitElement {
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchKeys=${deviceComboBoxKeys}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
.unknownItemText=${this._i18n.localize(
|
||||
"ui.components.device-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -466,7 +555,9 @@ export class HaDevicePicker extends LitElement {
|
||||
// The split devices all inherit the composite's name, so the integration is
|
||||
// what tells them apart.
|
||||
const replacementDevice =
|
||||
candidates.length === 1 ? this.hass.devices[candidates[0]] : undefined;
|
||||
candidates.length === 1
|
||||
? this._registries.devices[candidates[0]]
|
||||
: undefined;
|
||||
const replacementName = replacementDevice
|
||||
? computeDeviceName(replacementDevice)
|
||||
: undefined;
|
||||
@@ -478,22 +569,22 @@ export class HaDevicePicker extends LitElement {
|
||||
<ha-alert alert-type="warning">
|
||||
${
|
||||
replacementName && replacementDomain
|
||||
? this.hass.localize(
|
||||
? this._i18n.localize(
|
||||
"ui.components.device-picker.device_replaced_by_one_integration",
|
||||
{
|
||||
device: replacementName,
|
||||
integration: domainToName(
|
||||
this.hass.localize,
|
||||
this._i18n.localize,
|
||||
replacementDomain
|
||||
),
|
||||
}
|
||||
)
|
||||
: replacementName
|
||||
? this.hass.localize(
|
||||
? this._i18n.localize(
|
||||
"ui.components.device-picker.device_replaced_by_one",
|
||||
{ device: replacementName }
|
||||
)
|
||||
: this.hass.localize(
|
||||
: this._i18n.localize(
|
||||
"ui.components.device-picker.device_replaced_by_multiple",
|
||||
{ count: candidates.length }
|
||||
)
|
||||
@@ -506,8 +597,12 @@ export class HaDevicePicker extends LitElement {
|
||||
>
|
||||
${
|
||||
candidates.length === 1
|
||||
? this.hass.localize("ui.components.device-picker.replace_update")
|
||||
: this.hass.localize("ui.components.device-picker.replace_choose")
|
||||
? this._i18n.localize(
|
||||
"ui.components.device-picker.replace_update"
|
||||
)
|
||||
: this._i18n.localize(
|
||||
"ui.components.device-picker.replace_choose"
|
||||
)
|
||||
}
|
||||
</ha-button>
|
||||
</ha-alert>
|
||||
@@ -517,7 +612,7 @@ export class HaDevicePicker extends LitElement {
|
||||
private _handleReplace = () => {
|
||||
const replacement = this._getReplacement(
|
||||
this.value,
|
||||
this.hass.devices,
|
||||
this._registries.devices,
|
||||
this._compositeSplits,
|
||||
this._getItems(),
|
||||
this.replacementDeviceIds
|
||||
@@ -556,7 +651,7 @@ export class HaDevicePicker extends LitElement {
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.device-picker.no_match", {
|
||||
this._i18n.localize("ui.components.device-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||
@customElement("ha-device-trigger-picker")
|
||||
class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
|
||||
protected get NO_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.triggers.no_triggers"
|
||||
);
|
||||
}
|
||||
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.triggers.unknown_trigger"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { ValueChangedEvent } from "../../types";
|
||||
import "./ha-device-picker";
|
||||
import type {
|
||||
HaDevicePickerDeviceFilterFunc,
|
||||
@@ -10,8 +10,6 @@ import type {
|
||||
|
||||
@customElement("ha-devices-picker")
|
||||
class HaDevicesPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property() public helper?: string;
|
||||
@@ -51,10 +49,6 @@ class HaDevicesPicker extends LitElement {
|
||||
public entityFilter?: HaDevicePickerEntityFilterFunc;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const currentDevices = this._currentDevices;
|
||||
return html`
|
||||
${currentDevices.map(
|
||||
@@ -62,7 +56,6 @@ class HaDevicesPicker extends LitElement {
|
||||
<div>
|
||||
<ha-device-picker
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@@ -78,7 +71,6 @@ class HaDevicesPicker extends LitElement {
|
||||
)}
|
||||
<div>
|
||||
<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import {
|
||||
consumeEntityStates,
|
||||
consumeLocalize,
|
||||
} from "../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { formattersContext } from "../../data/context";
|
||||
import type { HomeAssistantFormatters, ValueChangedEvent } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
type FormatEntityAttributeName =
|
||||
HomeAssistantFormatters["formatEntityAttributeName"];
|
||||
|
||||
@customElement("ha-entity-attribute-picker")
|
||||
class HaEntityAttributePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string | string[];
|
||||
|
||||
/**
|
||||
@@ -38,18 +48,32 @@ class HaEntityAttributePicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state()
|
||||
@consumeEntityStates({ entityIdPath: ["entityId"] })
|
||||
private _stateObjs?: Record<string, HassEntity>;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
@transform<HomeAssistantFormatters, FormatEntityAttributeName>({
|
||||
transformer: (formatters) => formatters?.formatEntityAttributeName,
|
||||
})
|
||||
private _formatEntityAttributeName!: FormatEntityAttributeName;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
private _getItemsMemoized = memoizeOne(
|
||||
(
|
||||
entityId: string | string[] | undefined,
|
||||
stateObjs: Record<string, HassEntity> | undefined,
|
||||
hideAttributes: string[] | undefined,
|
||||
hass: HomeAssistant
|
||||
formatEntityAttributeName: FormatEntityAttributeName
|
||||
): PickerComboBoxItem[] => {
|
||||
const entityIds = entityId ? ensureArray(entityId) : [];
|
||||
const options: PickerComboBoxItem[] = [];
|
||||
const optionsSet = new Set<string>();
|
||||
|
||||
for (const id of entityIds) {
|
||||
const stateObj = hass.states[id];
|
||||
const stateObj = stateObjs?.[id];
|
||||
if (!stateObj) {
|
||||
continue;
|
||||
}
|
||||
@@ -63,7 +87,7 @@ class HaEntityAttributePicker extends LitElement {
|
||||
optionsSet.add(attribute);
|
||||
options.push({
|
||||
id: attribute,
|
||||
primary: hass.formatEntityAttributeName(stateObj, attribute),
|
||||
primary: formatEntityAttributeName(stateObj, attribute),
|
||||
sorting_label: attribute,
|
||||
});
|
||||
}
|
||||
@@ -75,19 +99,28 @@ class HaEntityAttributePicker extends LitElement {
|
||||
);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getItemsMemoized(this.entityId, this.hideAttributes, this.hass);
|
||||
this._getItemsMemoized(
|
||||
this.entityId,
|
||||
this._stateObjs,
|
||||
this.hideAttributes,
|
||||
this._formatEntityAttributeName
|
||||
);
|
||||
|
||||
private _valueRenderer = memoizeOne(
|
||||
(items: PickerComboBoxItem[]): PickerValueRenderer =>
|
||||
(value: string) => {
|
||||
const item = items.find((option) => option.id === value);
|
||||
return html`<span slot="headline">${item?.primary ?? value}</span>`;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.value=${this.value}
|
||||
.label=${
|
||||
this.label ??
|
||||
this.hass.localize(
|
||||
this._localize(
|
||||
"ui.components.entity.entity-attribute-picker.attribute"
|
||||
)
|
||||
}
|
||||
@@ -96,19 +129,13 @@ class HaEntityAttributePicker extends LitElement {
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.getItems=${this._getItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.valueRenderer=${this._valueRenderer(this._getItems())}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const items = this._getItems();
|
||||
const item = items.find((option) => option.id === value);
|
||||
return html`<span slot="headline">${item?.primary ?? value}</span>`;
|
||||
};
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiDragHorizontalVariant, mdiPlus, mdiRestore } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -8,6 +9,12 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import {
|
||||
consumeEntityState,
|
||||
consumeLocalize,
|
||||
} from "../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
computeDefaultEntityNameItems,
|
||||
@@ -16,11 +23,15 @@ import {
|
||||
type EntityNameType,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-assist-chip";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import { formattersContext, registriesContext } from "../../data/context";
|
||||
import type { HomeAssistantFormatters, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-button";
|
||||
import "../ha-button-toggle-group";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
@@ -68,8 +79,6 @@ const parseOptionValue = (value: string): EntityNameItem => {
|
||||
|
||||
@customElement("ha-entity-name-picker")
|
||||
export class HaEntityNamePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
@property({ attribute: false }) public value?:
|
||||
@@ -90,8 +99,30 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
@state() private _mode?: "composed" | "custom";
|
||||
|
||||
@state()
|
||||
@consumeEntityState({ entityIdPath: ["entityId"] })
|
||||
private _stateObj?: HassEntity;
|
||||
|
||||
@state()
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries!: ContextType<typeof registriesContext>;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
@transform<
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantFormatters["formatEntityName"]
|
||||
>({
|
||||
transformer: ({ formatEntityName }) => formatEntityName,
|
||||
})
|
||||
private _formatEntityName!: HomeAssistantFormatters["formatEntityName"];
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@query("ha-button.add") private _addButton?: HTMLElement;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
connectedCallback(): void {
|
||||
@@ -114,13 +145,13 @@ export class HaEntityNamePicker extends LitElement {
|
||||
protected render() {
|
||||
const modeButtons = [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
label: this._localize(
|
||||
"ui.components.entity.entity-name-picker.mode_composed"
|
||||
),
|
||||
value: "composed",
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
label: this._localize(
|
||||
"ui.components.entity.entity-name-picker.mode_custom"
|
||||
),
|
||||
value: "custom",
|
||||
@@ -130,7 +161,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
${this.label ? html`<label id="label">${this.label}</label>` : nothing}
|
||||
${
|
||||
this.allowCustom
|
||||
? html`
|
||||
@@ -167,9 +198,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
const items = this._items;
|
||||
const value =
|
||||
items.length === 1 && items[0].type === "text" ? items[0].text || "" : "";
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const stateObj = this._stateObj;
|
||||
return html`
|
||||
<ha-input
|
||||
.disabled=${this.disabled}
|
||||
@@ -177,10 +206,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
.value=${value}
|
||||
.placeholder=${
|
||||
stateObj
|
||||
? this.hass.formatEntityName(
|
||||
stateObj,
|
||||
this.value ?? this.defaultValue
|
||||
)
|
||||
? this._formatEntityName(stateObj, this.value ?? this.defaultValue)
|
||||
: undefined
|
||||
}
|
||||
@input=${this._textInputChanged}
|
||||
@@ -199,7 +225,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
class="restore"
|
||||
slot=${ifDefined(slot)}
|
||||
.path=${mdiRestore}
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.components.entity.entity-name-picker.restore"
|
||||
)}
|
||||
@click=${this._restore}
|
||||
@@ -209,7 +235,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
private _renderPicker() {
|
||||
const value = this._items;
|
||||
const validTypes = this._validTypes(this.entityId);
|
||||
const validTypes = this._validTypes(this._stateObj, this._registries);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
@@ -220,15 +246,15 @@ export class HaEntityNamePicker extends LitElement {
|
||||
.value=${this._getPickerValue()}
|
||||
no-sort
|
||||
?allow-custom-value=${this.allowCustom}
|
||||
.customValueLabel=${this.hass.localize(
|
||||
.customValueLabel=${this._localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
)}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
.emptyLabel=${this._localize(
|
||||
"ui.components.entity.entity-name-picker.no_more_parts"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
.searchFn=${this._searchFn}
|
||||
.searchLabel=${this.hass.localize(
|
||||
.searchLabel=${this._localize(
|
||||
"ui.components.entity.entity-name-picker.search"
|
||||
)}
|
||||
>
|
||||
@@ -240,7 +266,9 @@ export class HaEntityNamePicker extends LitElement {
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
<ha-chip-set
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item,
|
||||
@@ -266,22 +294,16 @@ export class HaEntityNamePicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${
|
||||
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-button
|
||||
class="add"
|
||||
size="s"
|
||||
appearance="filled"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._addItem}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this._localize("ui.common.add")}
|
||||
</ha-button>
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
${this._renderRestoreButton()}
|
||||
@@ -349,6 +371,10 @@ export class HaEntityNamePicker extends LitElement {
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
if (!value.length) {
|
||||
await this.updateComplete;
|
||||
this._addButton?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
@@ -392,32 +418,18 @@ export class HaEntityNamePicker extends LitElement {
|
||||
if (this.defaultValue !== undefined) {
|
||||
return this._toItems(this.defaultValue);
|
||||
}
|
||||
return this._defaultItems(
|
||||
this.entityId ? this.hass.states[this.entityId] : undefined,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
return this._defaultItems(this._stateObj, this._registries);
|
||||
}
|
||||
|
||||
private _defaultItems = memoizeOne(
|
||||
(
|
||||
stateObj: HassEntity | undefined,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"]
|
||||
registries: ContextType<typeof registriesContext>
|
||||
): EntityNameItem[] => {
|
||||
if (!stateObj) {
|
||||
return DEFAULT_NAME_WITHOUT_ENTITY;
|
||||
}
|
||||
return computeDefaultEntityNameItems(stateObj, {
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors,
|
||||
});
|
||||
return computeDefaultEntityNameItems(stateObj, registries);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -449,81 +461,88 @@ export class HaEntityNamePicker extends LitElement {
|
||||
return `"${item.text}"`;
|
||||
}
|
||||
if (KNOWN_TYPES.has(item.type)) {
|
||||
return this.hass.localize(
|
||||
return this._localize(
|
||||
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
|
||||
);
|
||||
}
|
||||
return item.type;
|
||||
};
|
||||
|
||||
private _validTypes = memoizeOne((entityId?: string) => {
|
||||
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||
private _validTypes = memoizeOne(
|
||||
(
|
||||
stateObj: HassEntity | undefined,
|
||||
registries: ContextType<typeof registriesContext>
|
||||
) => {
|
||||
if (!stateObj) {
|
||||
return new Set<string>(["text", ...ENTITY_NAME_TYPES]);
|
||||
}
|
||||
|
||||
if (!stateObj) {
|
||||
return new Set<string>(["text", ...ENTITY_NAME_TYPES]);
|
||||
}
|
||||
const options = new Set<string>(["text", "entity"]);
|
||||
|
||||
const options = new Set<string>(["text", "entity"]);
|
||||
|
||||
const context = getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
if (context.device) options.add("device");
|
||||
if (context.parentDevice) options.add("parent_device");
|
||||
if (context.area) options.add("area");
|
||||
if (context.floor) options.add("floor");
|
||||
return options;
|
||||
});
|
||||
|
||||
private _getItems = memoizeOne((entityId?: string) => {
|
||||
const types = this._validTypes(entityId);
|
||||
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||
|
||||
const items = ENTITY_NAME_TYPES.filter(
|
||||
(name) => name !== "parent_device" || types.has(name)
|
||||
).map<PickerComboBoxItem>((name) => {
|
||||
const isValid = types.has(name);
|
||||
const primary = this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${name}`
|
||||
const context = getEntityContext(
|
||||
stateObj,
|
||||
registries.entities,
|
||||
registries.devices,
|
||||
registries.areas,
|
||||
registries.floors
|
||||
);
|
||||
const secondary = stateObj
|
||||
? (isValid
|
||||
? this.hass.formatEntityName(stateObj, { type: name })
|
||||
: this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
|
||||
)) || "-"
|
||||
: undefined;
|
||||
|
||||
const id = formatOptionValue({ type: name });
|
||||
if (context.device) options.add("device");
|
||||
if (context.parentDevice) options.add("parent_device");
|
||||
if (context.area) options.add("area");
|
||||
if (context.floor) options.add("floor");
|
||||
return options;
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
primary,
|
||||
secondary,
|
||||
disabled: !isValid,
|
||||
search_labels: {
|
||||
primary,
|
||||
secondary: secondary || null,
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
stateObj: HassEntity | undefined,
|
||||
registries: ContextType<typeof registriesContext>,
|
||||
localize: LocalizeFunc,
|
||||
formatEntityName: HomeAssistantFormatters["formatEntityName"]
|
||||
) => {
|
||||
const types = this._validTypes(stateObj, registries);
|
||||
|
||||
const items = ENTITY_NAME_TYPES.filter(
|
||||
(name) => name !== "parent_device" || types.has(name)
|
||||
).map<PickerComboBoxItem>((name) => {
|
||||
const isValid = types.has(name);
|
||||
const primary = localize(
|
||||
`ui.components.entity.entity-name-picker.types.${name}`
|
||||
);
|
||||
const secondary = stateObj
|
||||
? (isValid
|
||||
? formatEntityName(stateObj, { type: name })
|
||||
: localize(
|
||||
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
|
||||
)) || "-"
|
||||
: undefined;
|
||||
|
||||
const id = formatOptionValue({ type: name });
|
||||
|
||||
return {
|
||||
id,
|
||||
},
|
||||
sorting_label: primary,
|
||||
};
|
||||
});
|
||||
primary,
|
||||
secondary,
|
||||
disabled: !isValid,
|
||||
search_labels: {
|
||||
primary,
|
||||
secondary: secondary || null,
|
||||
id,
|
||||
},
|
||||
sorting_label: primary,
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
});
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _customNameOption = memoizeOne(
|
||||
(text: string): PickerComboBoxItem => ({
|
||||
(text: string, localize: LocalizeFunc): PickerComboBoxItem => ({
|
||||
id: formatOptionValue({ type: "text", text }),
|
||||
primary: this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
),
|
||||
primary: localize("ui.components.entity.entity-name-picker.custom_name"),
|
||||
secondary: `"${text}"`,
|
||||
search_labels: {
|
||||
primary: text,
|
||||
@@ -543,7 +562,12 @@ export class HaEntityNamePicker extends LitElement {
|
||||
}
|
||||
|
||||
private _getFilteredItems = (): PickerComboBoxItem[] => {
|
||||
const items = this._getItems(this.entityId);
|
||||
const items = this._getItems(
|
||||
this._stateObj,
|
||||
this._registries,
|
||||
this._localize,
|
||||
this._formatEntityName
|
||||
);
|
||||
const currentItem =
|
||||
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
||||
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
|
||||
@@ -560,7 +584,9 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
// When editing an existing text item, include it in the base items
|
||||
if (currentItem?.type === "text" && currentItem.text) {
|
||||
filteredItems.push(this._customNameOption(currentItem.text));
|
||||
filteredItems.push(
|
||||
this._customNameOption(currentItem.text, this._localize)
|
||||
);
|
||||
}
|
||||
|
||||
return filteredItems;
|
||||
@@ -574,7 +600,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
||||
const currentId =
|
||||
currentItem?.type === "text" && currentItem.text
|
||||
? this._customNameOption(currentItem.text).id
|
||||
? this._customNameOption(currentItem.text, this._localize).id
|
||||
: undefined;
|
||||
|
||||
// Remove custom name option if search string is present to avoid duplicates
|
||||
@@ -605,7 +631,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -698,7 +724,8 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
ha-input-helper-text {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
padding: 0;
|
||||
margin: var(--ha-space-1) 0 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -253,7 +253,10 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
{
|
||||
...this._registries,
|
||||
entities: this._registries.entities,
|
||||
devices: this._registries.devices,
|
||||
areas: this._registries.areas,
|
||||
floors: this._registries.floors,
|
||||
language: this._i18n.language,
|
||||
translationMetadata: this._i18n.translationMetadata,
|
||||
},
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
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 { consume } from "../../common/decorators/consume";
|
||||
import {
|
||||
consumeEntityState,
|
||||
consumeLocalize,
|
||||
} from "../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { formattersContext, registriesContext } from "../../data/context";
|
||||
import {
|
||||
STATE_DISPLAY_SPECIAL_CONTENT,
|
||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
|
||||
} from "../../state-display/state-display";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { HomeAssistantFormatters, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
@@ -79,10 +88,11 @@ const HIDDEN_ATTRIBUTES = [
|
||||
"wind_speed_unit",
|
||||
];
|
||||
|
||||
type FormatEntityAttributeName =
|
||||
HomeAssistantFormatters["formatEntityAttributeName"];
|
||||
|
||||
@customElement("ha-entity-state-content-picker")
|
||||
export class HaStateContentPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@@ -106,34 +116,48 @@ export class HaStateContentPicker extends LitElement {
|
||||
|
||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||
|
||||
@state()
|
||||
@consumeEntityState({ entityIdPath: ["entityId"] })
|
||||
private _stateObj?: HassEntity;
|
||||
|
||||
@state()
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries!: ContextType<typeof registriesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
@transform<HomeAssistantFormatters, FormatEntityAttributeName>({
|
||||
transformer: (formatters) => formatters?.formatEntityAttributeName,
|
||||
})
|
||||
private _formatEntityAttributeName!: FormatEntityAttributeName;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
entityId?: string,
|
||||
stateObj?: HassEntity,
|
||||
allowName?: boolean,
|
||||
allowContext?: boolean
|
||||
entityId: string | undefined,
|
||||
stateObj: HassEntity | undefined,
|
||||
allowName: boolean,
|
||||
allowContext: boolean,
|
||||
localize: LocalizeFunc,
|
||||
registries: ContextType<typeof registriesContext>,
|
||||
formatEntityAttributeName: FormatEntityAttributeName
|
||||
) => {
|
||||
const domain = entityId ? computeDomain(entityId) : undefined;
|
||||
const items: PickerComboBoxItem[] = [
|
||||
{
|
||||
id: "state",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.state"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.state"
|
||||
),
|
||||
primary: localize("ui.components.state-content-picker.state"),
|
||||
sorting_label: localize("ui.components.state-content-picker.state"),
|
||||
},
|
||||
...(allowName
|
||||
? [
|
||||
{
|
||||
id: "name",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
primary: localize("ui.components.state-content-picker.name"),
|
||||
sorting_label: localize(
|
||||
"ui.components.state-content-picker.name"
|
||||
),
|
||||
} satisfies PickerComboBoxItem,
|
||||
@@ -141,19 +165,15 @@ export class HaStateContentPicker extends LitElement {
|
||||
: []),
|
||||
{
|
||||
id: "last_changed",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.last_changed"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
primary: localize("ui.components.state-content-picker.last_changed"),
|
||||
sorting_label: localize(
|
||||
"ui.components.state-content-picker.last_changed"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "last_updated",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.last_updated"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
primary: localize("ui.components.state-content-picker.last_updated"),
|
||||
sorting_label: localize(
|
||||
"ui.components.state-content-picker.last_updated"
|
||||
),
|
||||
},
|
||||
@@ -161,19 +181,19 @@ export class HaStateContentPicker extends LitElement {
|
||||
? (() => {
|
||||
const context = getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
registries.entities,
|
||||
registries.devices,
|
||||
registries.areas,
|
||||
registries.floors
|
||||
);
|
||||
const contextItems: PickerComboBoxItem[] = [];
|
||||
if (context.device) {
|
||||
contextItems.push({
|
||||
id: "device_name",
|
||||
primary: this.hass.localize(
|
||||
primary: localize(
|
||||
"ui.components.state-content-picker.device_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
sorting_label: localize(
|
||||
"ui.components.state-content-picker.device_name"
|
||||
),
|
||||
});
|
||||
@@ -181,10 +201,10 @@ export class HaStateContentPicker extends LitElement {
|
||||
if (context.parentDevice) {
|
||||
contextItems.push({
|
||||
id: "parent_device_name",
|
||||
primary: this.hass.localize(
|
||||
primary: localize(
|
||||
"ui.components.state-content-picker.parent_device_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
sorting_label: localize(
|
||||
"ui.components.state-content-picker.parent_device_name"
|
||||
),
|
||||
});
|
||||
@@ -192,10 +212,10 @@ export class HaStateContentPicker extends LitElement {
|
||||
if (context.area) {
|
||||
contextItems.push({
|
||||
id: "area_name",
|
||||
primary: this.hass.localize(
|
||||
primary: localize(
|
||||
"ui.components.state-content-picker.area_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
sorting_label: localize(
|
||||
"ui.components.state-content-picker.area_name"
|
||||
),
|
||||
});
|
||||
@@ -203,10 +223,10 @@ export class HaStateContentPicker extends LitElement {
|
||||
if (context.floor) {
|
||||
contextItems.push({
|
||||
id: "floor_name",
|
||||
primary: this.hass.localize(
|
||||
primary: localize(
|
||||
"ui.components.state-content-picker.floor_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
sorting_label: localize(
|
||||
"ui.components.state-content-picker.floor_name"
|
||||
),
|
||||
});
|
||||
@@ -221,10 +241,10 @@ export class HaStateContentPicker extends LitElement {
|
||||
(content) =>
|
||||
({
|
||||
id: content,
|
||||
primary: this.hass.localize(
|
||||
primary: localize(
|
||||
`ui.components.state-content-picker.${content}`
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
sorting_label: localize(
|
||||
`ui.components.state-content-picker.${content}`
|
||||
),
|
||||
}) satisfies PickerComboBoxItem
|
||||
@@ -236,14 +256,8 @@ export class HaStateContentPicker extends LitElement {
|
||||
(attribute) =>
|
||||
({
|
||||
id: attribute,
|
||||
primary: this.hass.formatEntityAttributeName(
|
||||
stateObj!,
|
||||
attribute
|
||||
),
|
||||
sorting_label: this.hass.formatEntityAttributeName(
|
||||
stateObj!,
|
||||
attribute
|
||||
),
|
||||
primary: formatEntityAttributeName(stateObj!, attribute),
|
||||
sorting_label: formatEntityAttributeName(stateObj!, attribute),
|
||||
}) satisfies PickerComboBoxItem
|
||||
),
|
||||
];
|
||||
@@ -251,13 +265,21 @@ export class HaStateContentPicker extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _items() {
|
||||
return this._getItems(
|
||||
this.entityId,
|
||||
this._stateObj,
|
||||
this.allowName,
|
||||
this.allowContext,
|
||||
this._localize,
|
||||
this._registries,
|
||||
this._formatEntityAttributeName
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const value = this._value;
|
||||
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-generic-picker
|
||||
@@ -282,7 +304,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
this._value,
|
||||
(item) => item,
|
||||
(item: string, idx) => {
|
||||
const label = this._getItemLabel(item, stateObj);
|
||||
const label = this._getItemLabel(item);
|
||||
const isValid = !!label;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
@@ -309,7 +331,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
label=${this._localize(
|
||||
"ui.components.entity.entity-state-content-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
@@ -354,20 +376,9 @@ export class HaStateContentPicker extends LitElement {
|
||||
return !this.value ? [] : ensureArray(this.value);
|
||||
}
|
||||
|
||||
private _getItemLabel = memoizeOne(
|
||||
(value: string, stateObj?: HassEntity): string | undefined => {
|
||||
const stateObjForItems = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: stateObj;
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObjForItems,
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
);
|
||||
return items.find((item) => item.id === value)?.primary;
|
||||
}
|
||||
);
|
||||
private _getItemLabel(value: string): string | undefined {
|
||||
return this._items().find((item) => item.id === value)?.primary;
|
||||
}
|
||||
|
||||
private _toValue = memoizeOne((value: string[]): typeof this.value => {
|
||||
if (value.length === 0) {
|
||||
@@ -387,9 +398,9 @@ export class HaStateContentPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _customValueOption = memoizeOne(
|
||||
(text: string): PickerComboBoxItem => ({
|
||||
(text: string, localize: LocalizeFunc): PickerComboBoxItem => ({
|
||||
id: text,
|
||||
primary: this.hass.localize(
|
||||
primary: localize(
|
||||
"ui.components.entity.entity-state-content-picker.custom_attribute"
|
||||
),
|
||||
secondary: `"${text}"`,
|
||||
@@ -403,15 +414,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
);
|
||||
|
||||
private _getFilteredItems = (): PickerComboBoxItem[] => {
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObj,
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
);
|
||||
const items = this._items();
|
||||
const currentValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||
|
||||
@@ -423,7 +426,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
|
||||
// When editing an existing custom value, include it in the base items
|
||||
if (currentValue && !items.find((item) => item.id === currentValue)) {
|
||||
filteredItems.push(this._customValueOption(currentValue));
|
||||
filteredItems.push(this._customValueOption(currentValue, this._localize));
|
||||
}
|
||||
|
||||
return filteredItems;
|
||||
@@ -432,21 +435,13 @@ export class HaStateContentPicker extends LitElement {
|
||||
private _getAdditionalItems = (
|
||||
searchString?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObj,
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
);
|
||||
const items = this._items();
|
||||
|
||||
// If the search string does not match with the id of any of the items,
|
||||
// offer to add it as a custom attribute
|
||||
const existingItem = items.find((item) => item.id === searchString);
|
||||
if (searchString && !existingItem) {
|
||||
return [this._customValueOption(searchString)];
|
||||
return [this._customValueOption(searchString, this._localize)];
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { consumeEntityStates } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { getStates } from "../../common/entity/get_states";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import {
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import type { HomeAssistantFormatters, ValueChangedEvent } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
@customElement("ha-entity-state-picker")
|
||||
export class HaEntityStatePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string | string[];
|
||||
|
||||
@property() public attribute?: string;
|
||||
@@ -38,9 +45,26 @@ export class HaEntityStatePicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state()
|
||||
@consumeEntityStates({ entityIdPath: ["entityId"] })
|
||||
private _stateObjs?: Record<string, HassEntity>;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states?: ContextType<typeof statesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters!: ContextType<typeof formattersContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
states: HassEntities | null | undefined,
|
||||
formatters: HomeAssistantFormatters,
|
||||
language: string,
|
||||
entityId: string | string[] | undefined,
|
||||
attribute: string | undefined,
|
||||
hideStates: string[] | undefined,
|
||||
@@ -50,20 +74,22 @@ export class HaEntityStatePicker extends LitElement {
|
||||
|
||||
const entitiesOptions = entityIds.map<PickerComboBoxItem[]>(
|
||||
(entityIdItem) => {
|
||||
const stateObj = hass.states[entityIdItem] || {
|
||||
entity_id: entityIdItem,
|
||||
attributes: {},
|
||||
};
|
||||
const stateObj =
|
||||
states?.[entityIdItem] ||
|
||||
({ entity_id: entityIdItem, attributes: {} } as HassEntity);
|
||||
|
||||
const states = getStates(hass, stateObj, attribute).filter(
|
||||
(s) => !hideStates?.includes(s)
|
||||
);
|
||||
const options = getStates(
|
||||
states ?? {},
|
||||
language,
|
||||
stateObj,
|
||||
attribute
|
||||
).filter((s) => !hideStates?.includes(s));
|
||||
|
||||
return states
|
||||
return options
|
||||
.map((s) => {
|
||||
const primary = attribute
|
||||
? hass.formatEntityAttributeValue(stateObj, attribute, s)
|
||||
: hass.formatEntityState(stateObj, s);
|
||||
? formatters.formatEntityAttributeValue(stateObj, attribute, s)
|
||||
: formatters.formatEntityState(stateObj, s);
|
||||
return {
|
||||
id: s,
|
||||
primary,
|
||||
@@ -102,7 +128,9 @@ export class HaEntityStatePicker extends LitElement {
|
||||
_section?: string
|
||||
): PickerComboBoxItem[] =>
|
||||
this._getItems(
|
||||
this.hass,
|
||||
this._states,
|
||||
this._formatters,
|
||||
this._i18n.locale.language,
|
||||
this.entityId,
|
||||
this.attribute,
|
||||
this.hideStates,
|
||||
@@ -121,19 +149,20 @@ export class HaEntityStatePicker extends LitElement {
|
||||
// entity to resolve the translated name; otherwise fall back to "State".
|
||||
if (this.attribute && this.entityId) {
|
||||
const entityId = ensureArray(this.entityId)[0];
|
||||
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||
const stateObj = entityId ? this._stateObjs?.[entityId] : undefined;
|
||||
if (stateObj) {
|
||||
return this.hass.formatEntityAttributeName(stateObj, this.attribute);
|
||||
return this._formatters.formatEntityAttributeName(
|
||||
stateObj,
|
||||
this.attribute
|
||||
);
|
||||
}
|
||||
}
|
||||
return this.hass.localize("ui.components.entity.entity-state-picker.state");
|
||||
return this._i18n.localize(
|
||||
"ui.components.entity.entity-state-picker.state"
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const noEntity = !ensureArray(this.entityId)?.length;
|
||||
|
||||
return html`
|
||||
@@ -147,8 +176,10 @@ export class HaEntityStatePicker extends LitElement {
|
||||
.value=${this.value}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
|
||||
.customValueLabel=${this.hass.localize(
|
||||
.notFoundLabel=${this._i18n.localize(
|
||||
"ui.components.combo-box.no_match"
|
||||
)}
|
||||
.customValueLabel=${this._i18n.localize(
|
||||
"ui.components.entity.entity-state-picker.add_custom_state"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
|
||||
@@ -6,13 +6,10 @@ import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { ANY_STATE_VALUE } from "./const";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-entity-state-picker";
|
||||
|
||||
@customElement("ha-entity-states-picker")
|
||||
export class HaEntityStatesPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
@property() public attribute?: string;
|
||||
@@ -52,10 +49,6 @@ export class HaEntityStatesPicker extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const value = this.value || [];
|
||||
const hide = [...(this.hideStates || []), ...value];
|
||||
const hideValue = value.includes(ANY_STATE_VALUE);
|
||||
@@ -68,7 +61,6 @@ export class HaEntityStatesPicker extends LitElement {
|
||||
<div>
|
||||
<ha-entity-state-picker
|
||||
.index=${index}
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.entityId}
|
||||
.attribute=${this.attribute}
|
||||
.extraOptions=${this.extraOptions}
|
||||
@@ -94,7 +86,6 @@ export class HaEntityStatesPicker extends LitElement {
|
||||
: keyed(
|
||||
value.length,
|
||||
html`<ha-entity-state-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.entityId}
|
||||
.attribute=${this.attribute}
|
||||
.extraOptions=${this.extraOptions}
|
||||
|
||||
@@ -47,7 +47,10 @@ export class HaFavoriteEntityListItem extends LitElement {
|
||||
const { primary, secondary } = stateObj
|
||||
? computeEntityPickerDisplay(
|
||||
{
|
||||
...this._registries,
|
||||
entities: this._registries.entities,
|
||||
devices: this._registries.devices,
|
||||
areas: this._registries.areas,
|
||||
floors: this._registries.floors,
|
||||
language: this._i18n.language,
|
||||
translationMetadata: this._i18n.translationMetadata,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import {
|
||||
mdiChartLine,
|
||||
mdiHelpCircleOutline,
|
||||
@@ -7,21 +8,31 @@ import {
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { type HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
computeEntityPickerDisplay,
|
||||
computeEntitySearchLabels,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import {
|
||||
apiContext,
|
||||
configContext,
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
getStatisticLabel,
|
||||
type StatisticsMetaData,
|
||||
} from "../../data/recorder";
|
||||
import { getStatisticIds } from "../../data/recorder_statistic_ids";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { HomeAssistantFormatters, ValueChangedEvent } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
@@ -72,7 +83,34 @@ declare global {
|
||||
|
||||
@customElement("ha-statistic-picker")
|
||||
export class HaStatisticPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries!: ContextType<typeof registriesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
@transform<
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantFormatters["formatEntityName"]
|
||||
>({
|
||||
transformer: (formatters) => formatters?.formatEntityName,
|
||||
})
|
||||
private _formatEntityName!: HomeAssistantFormatters["formatEntityName"];
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
@@ -163,14 +201,17 @@ export class HaStatisticPicker extends LitElement {
|
||||
}
|
||||
|
||||
private async _getStatisticIds() {
|
||||
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
||||
this.statisticIds = await getStatisticIds(this._api, this.statisticTypes);
|
||||
this._picker?.requestUpdate();
|
||||
this._valueRenderer = this._makeValueRenderer();
|
||||
}
|
||||
|
||||
private _getItems = () =>
|
||||
this._getStatisticsItems(
|
||||
this.hass,
|
||||
this._states,
|
||||
this._registries,
|
||||
this._i18n,
|
||||
this._formatEntityName,
|
||||
this.statisticIds,
|
||||
this.includeStatisticsUnitOfMeasurement,
|
||||
this.includeUnitClass,
|
||||
@@ -183,7 +224,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
private _getAdditionalItems = (): StatisticComboBoxItem[] => [
|
||||
{
|
||||
id: MISSING_ID,
|
||||
primary: this.hass.localize(
|
||||
primary: this._i18n.localize(
|
||||
"ui.components.statistic-picker.missing_entity"
|
||||
),
|
||||
icon_path: mdiHelpCircleOutline,
|
||||
@@ -192,7 +233,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
private _getStatisticsItems = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
states: ContextType<typeof statesContext>,
|
||||
registries: ContextType<typeof registriesContext>,
|
||||
i18n: ContextType<typeof internationalizationContext>,
|
||||
formatEntityName: HomeAssistantFormatters["formatEntityName"],
|
||||
statisticIds?: StatisticsMetaData[],
|
||||
includeStatisticsUnitOfMeasurement?: string | string[],
|
||||
includeUnitClass?: string | string[],
|
||||
@@ -201,7 +245,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
excludeStatistics?: string[],
|
||||
value?: string
|
||||
): StatisticComboBoxItem[] | undefined => {
|
||||
if (!statisticIds) {
|
||||
if (!statisticIds || !states || !registries) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -224,7 +268,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
const includeDeviceClasses: (string | null)[] =
|
||||
ensureArray(includeDeviceClass);
|
||||
statisticIds = statisticIds.filter((meta) => {
|
||||
const stateObj = this.hass.states[meta.statistic_id];
|
||||
const stateObj = states[meta.statistic_id];
|
||||
if (!stateObj) {
|
||||
return true;
|
||||
}
|
||||
@@ -244,14 +288,14 @@ export class HaStatisticPicker extends LitElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[meta.statistic_id];
|
||||
const stateObj = states[meta.statistic_id];
|
||||
|
||||
if (!stateObj) {
|
||||
if (!entitiesOnly) {
|
||||
const id = meta.statistic_id;
|
||||
const label = getStatisticLabel(
|
||||
this.hass.states,
|
||||
this.hass.formatEntityName,
|
||||
states,
|
||||
formatEntityName,
|
||||
meta.statistic_id,
|
||||
meta
|
||||
);
|
||||
@@ -266,7 +310,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
output.push({
|
||||
id,
|
||||
primary: label,
|
||||
secondary: this.hass.localize(
|
||||
secondary: i18n.localize(
|
||||
"ui.components.statistic-picker.no_state"
|
||||
),
|
||||
type,
|
||||
@@ -275,7 +319,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
});
|
||||
} else if (type === "external") {
|
||||
const domain = id.split(":")[0];
|
||||
const domainName = domainToName(this.hass.localize, domain);
|
||||
const domainName = domainToName(i18n.localize, domain);
|
||||
output.push({
|
||||
id,
|
||||
statistic_id: id,
|
||||
@@ -293,10 +337,17 @@ export class HaStatisticPicker extends LitElement {
|
||||
const id = meta.statistic_id;
|
||||
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
hass,
|
||||
{
|
||||
entities: registries.entities,
|
||||
devices: registries.devices,
|
||||
areas: registries.areas,
|
||||
floors: registries.floors,
|
||||
language: i18n.language,
|
||||
translationMetadata: i18n.translationMetadata,
|
||||
},
|
||||
stateObj
|
||||
);
|
||||
const searchLabels = computeEntitySearchLabels(stateObj, hass);
|
||||
const searchLabels = computeEntitySearchLabels(stateObj, registries);
|
||||
|
||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
||||
output.push({
|
||||
@@ -361,7 +412,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
? html`<ha-icon-button
|
||||
slot="end"
|
||||
.value=${statisticId}
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
.label=${this._i18n.localize("ui.common.edit")}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._editItem}
|
||||
></ha-icon-button>`
|
||||
@@ -383,14 +434,24 @@ export class HaStatisticPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _computeItem(statisticId: string): StatisticComboBoxItem {
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const stateObj = this._states?.[statisticId];
|
||||
|
||||
if (stateObj) {
|
||||
if (stateObj && this._registries) {
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
this.hass,
|
||||
{
|
||||
entities: this._registries.entities,
|
||||
devices: this._registries.devices,
|
||||
areas: this._registries.areas,
|
||||
floors: this._registries.floors,
|
||||
language: this._i18n.language,
|
||||
translationMetadata: this._i18n.translationMetadata,
|
||||
},
|
||||
stateObj
|
||||
);
|
||||
const searchLabels = computeEntitySearchLabels(stateObj, this.hass);
|
||||
const searchLabels = computeEntitySearchLabels(
|
||||
stateObj,
|
||||
this._registries
|
||||
);
|
||||
|
||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
||||
return {
|
||||
@@ -422,13 +483,13 @@ export class HaStatisticPicker extends LitElement {
|
||||
if (type === "external") {
|
||||
const sortingPrefix = `${TYPE_ORDER.indexOf("external")}`;
|
||||
const label = getStatisticLabel(
|
||||
this.hass.states,
|
||||
this.hass.formatEntityName,
|
||||
this._states ?? {},
|
||||
this._formatEntityName,
|
||||
statisticId,
|
||||
statistic
|
||||
);
|
||||
const domain = statisticId.split(":")[0];
|
||||
const domainName = domainToName(this.hass.localize, domain);
|
||||
const domainName = domainToName(this._i18n.localize, domain);
|
||||
|
||||
return {
|
||||
id: statisticId,
|
||||
@@ -445,8 +506,8 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
const sortingPrefix = `${TYPE_ORDER.indexOf("external")}`;
|
||||
const label = getStatisticLabel(
|
||||
this.hass.states,
|
||||
this.hass.formatEntityName,
|
||||
this._states ?? {},
|
||||
this._formatEntityName,
|
||||
statisticId,
|
||||
statistic
|
||||
);
|
||||
@@ -454,7 +515,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
return {
|
||||
id: statisticId,
|
||||
primary: label,
|
||||
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
|
||||
secondary: this._i18n.localize("ui.components.statistic-picker.no_state"),
|
||||
type: "no_state",
|
||||
sorting_label: [sortingPrefix, label].join("_"),
|
||||
search_labels: { label, statisticId },
|
||||
@@ -466,7 +527,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
item,
|
||||
index
|
||||
) => {
|
||||
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||
const showEntityId = this._config?.userData?.showEntityIdPicker;
|
||||
return html`
|
||||
<ha-combo-box-item .borderTop=${index !== 0}>
|
||||
${
|
||||
@@ -507,7 +568,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
protected render() {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.statistic-picker.placeholder");
|
||||
this._i18n.localize("ui.components.statistic-picker.placeholder");
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
@@ -522,7 +583,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
.emptyLabel=${this._i18n.localize(
|
||||
"ui.components.statistic-picker.no_statistics"
|
||||
)}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
@@ -533,7 +594,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.helper=${this.helper}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
.unknownItemText=${this._i18n.localize(
|
||||
"ui.components.statistic-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -566,7 +627,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
if (value === MISSING_ID) {
|
||||
window.open(
|
||||
documentationUrl(this.hass, this.helpMissingEntityUrl),
|
||||
documentationUrl(this._config, this.helpMissingEntityUrl),
|
||||
"_blank"
|
||||
);
|
||||
return;
|
||||
@@ -582,7 +643,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.statistic-picker.no_match", {
|
||||
this._i18n.localize("ui.components.statistic-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,14 +2,12 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { type HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||
import type { ValueChangedEvent } from "../../types";
|
||||
import "./ha-statistic-picker";
|
||||
import type { StatisticElementChangedEvent } from "./ha-statistic-picker";
|
||||
|
||||
@customElement("ha-statistics-picker")
|
||||
class HaStatisticsPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property({ attribute: false }) public statisticIds?: string[];
|
||||
@@ -63,10 +61,6 @@ class HaStatisticsPicker extends LitElement {
|
||||
@property({ attribute: "can-edit", type: Boolean }) public canEdit?;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const ignoreRestriction =
|
||||
this.ignoreRestrictionsOnFirstStatistic &&
|
||||
this._currentStatistics.length <= 1;
|
||||
@@ -93,7 +87,6 @@ class HaStatisticsPicker extends LitElement {
|
||||
<div>
|
||||
<ha-statistic-picker
|
||||
.curValue=${statisticId}
|
||||
.hass=${this.hass}
|
||||
.includeStatisticsUnitOfMeasurement=${includeStatisticsUnitCurrent}
|
||||
.includeUnitClass=${includeUnitClassCurrent}
|
||||
.includeDeviceClass=${includeDeviceClassCurrent}
|
||||
@@ -111,7 +104,6 @@ class HaStatisticsPicker extends LitElement {
|
||||
)}
|
||||
<div>
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeStatisticsUnitOfMeasurement=${
|
||||
this.includeStatisticsUnitOfMeasurement
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import Fuse from "fuse.js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { computeEntityPickerDisplay } from "../common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
@@ -14,7 +15,11 @@ import {
|
||||
getAreaControlEntities,
|
||||
type AreaControlDomain,
|
||||
} from "../data/area/area_controls";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import {
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-domain-icon";
|
||||
@@ -46,8 +51,6 @@ const AREA_CONTROL_DOMAINS = [
|
||||
|
||||
@customElement("ha-area-controls-picker")
|
||||
export class HaAreaControlsPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "area-id" }) public areaId!: string;
|
||||
|
||||
@property({ type: Array, attribute: "exclude-entities" })
|
||||
@@ -70,6 +73,17 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
|
||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries!: ContextType<typeof registriesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
private _domainSearchKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
@@ -111,28 +125,38 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
excludeEntities: string[] | undefined,
|
||||
currentValue: string | undefined,
|
||||
excludeValues: string[] | undefined,
|
||||
localize: LocalizeFunc,
|
||||
_entities: HomeAssistant["entities"],
|
||||
_devices: HomeAssistant["devices"],
|
||||
_areas: HomeAssistant["areas"]
|
||||
registries: ContextType<typeof registriesContext>,
|
||||
i18n: ContextType<typeof internationalizationContext>
|
||||
): ((
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (AreaControlPickerItem | string)[]) =>
|
||||
(searchString?: string, section?: string) => {
|
||||
if (!this.hass) {
|
||||
const states = this._states;
|
||||
if (
|
||||
!states ||
|
||||
!registries?.entities ||
|
||||
!registries.devices ||
|
||||
!registries.areas ||
|
||||
!registries.floors
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const { localize } = i18n;
|
||||
|
||||
const isSelected = (id: string): boolean =>
|
||||
currentValue === id ||
|
||||
(excludeValues !== undefined && excludeValues.includes(id));
|
||||
|
||||
const controlEntities = getAreaControlEntities(
|
||||
states,
|
||||
registries.entities,
|
||||
registries.devices,
|
||||
registries.areas,
|
||||
registries.floors,
|
||||
AREA_CONTROL_DOMAINS,
|
||||
areaId,
|
||||
excludeEntities,
|
||||
this.hass
|
||||
excludeEntities
|
||||
);
|
||||
|
||||
const items: (AreaControlPickerItem | string)[] = [];
|
||||
@@ -186,13 +210,20 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
if (isSelected(entityId)) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass!.states[entityId];
|
||||
const stateObj = states[entityId];
|
||||
if (!stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
this.hass!,
|
||||
{
|
||||
entities: registries.entities,
|
||||
devices: registries.devices,
|
||||
areas: registries.areas,
|
||||
floors: registries.floors,
|
||||
language: i18n.language,
|
||||
translationMetadata: i18n.translationMetadata,
|
||||
},
|
||||
stateObj
|
||||
);
|
||||
|
||||
@@ -275,10 +306,6 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
`;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.value=${this.value || ""}
|
||||
@@ -293,22 +320,20 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
this.excludeEntities,
|
||||
this.value,
|
||||
this.excludeValues,
|
||||
this.hass.localize,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas
|
||||
this._registries,
|
||||
this._i18n
|
||||
)}
|
||||
.rowRenderer=${this._rowRenderer as any}
|
||||
.sections=${[
|
||||
{
|
||||
id: "domain",
|
||||
label: this.hass.localize(
|
||||
label: this._i18n.localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.sections.domain"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "entity",
|
||||
label: this.hass.localize(
|
||||
label: this._i18n.localize(
|
||||
"ui.panel.lovelace.editor.features.types.area-controls.sections.entity"
|
||||
),
|
||||
},
|
||||
|
||||
@@ -245,15 +245,15 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
const newOrder: string[] = [];
|
||||
|
||||
for (const floorId of floorIds) {
|
||||
const isOnFloor = (areaId: string) =>
|
||||
(this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR) === floorId;
|
||||
if ((currentFloorId ?? UNASSIGNED_FLOOR) === floorId) {
|
||||
newHidden.push(...(value.hidden ?? []));
|
||||
// Reordering returns the hidden areas of every floor, not just this one
|
||||
newHidden.push(...(value.hidden ?? []).filter(isOnFloor));
|
||||
newOrder.push(...(value.order ?? []));
|
||||
continue;
|
||||
}
|
||||
const hidden = oldHidden.filter((areaId) => {
|
||||
const id = this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
return id === floorId;
|
||||
});
|
||||
const hidden = oldHidden.filter(isOnFloor);
|
||||
if (hidden?.length) {
|
||||
newHidden.push(...hidden);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { formatLanguageCode } from "../common/language/format_language";
|
||||
import type { AssistPipeline } from "../data/assist_pipeline";
|
||||
import { listAssistPipelines } from "../data/assist_pipeline";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { apiContext, internationalizationContext } from "../data/context";
|
||||
import type { HomeAssistantInternationalization } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
|
||||
@@ -18,8 +21,6 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
@@ -30,6 +31,13 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
|
||||
@state() _preferredPipeline: string | null = null;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
private get _default() {
|
||||
return this.includeLastUsed ? LAST_USED : PREFERRED;
|
||||
}
|
||||
@@ -42,7 +50,7 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
const options: HaSelectOption[] = [
|
||||
{
|
||||
value: PREFERRED,
|
||||
label: this.hass.localize("ui.components.pipeline-picker.preferred", {
|
||||
label: this._i18n.localize("ui.components.pipeline-picker.preferred", {
|
||||
preferred: this._pipelines.find(
|
||||
(pipeline) => pipeline.id === this._preferredPipeline
|
||||
)?.name,
|
||||
@@ -53,14 +61,14 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
if (this.includeLastUsed) {
|
||||
options.unshift({
|
||||
value: LAST_USED,
|
||||
label: this.hass.localize("ui.components.pipeline-picker.last_used"),
|
||||
label: this._i18n.localize("ui.components.pipeline-picker.last_used"),
|
||||
});
|
||||
}
|
||||
|
||||
options.push(
|
||||
...this._pipelines.map((pipeline) => ({
|
||||
value: pipeline.id,
|
||||
label: `${pipeline.name} (${formatLanguageCode(pipeline.language, this.hass.locale)})`,
|
||||
label: `${pipeline.name} (${formatLanguageCode(pipeline.language, this._i18n.locale)})`,
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -68,7 +76,7 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
<ha-select
|
||||
.label=${
|
||||
this.label ||
|
||||
this.hass!.localize("ui.components.pipeline-picker.pipeline")
|
||||
this._i18n.localize("ui.components.pipeline-picker.pipeline")
|
||||
}
|
||||
.value=${value}
|
||||
.required=${this.required}
|
||||
@@ -82,7 +90,7 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
listAssistPipelines(this.hass).then((pipelines) => {
|
||||
listAssistPipelines(this._api.callWS).then((pipelines) => {
|
||||
this._pipelines = pipelines.pipelines;
|
||||
this._preferredPipeline = pipelines.preferred_pipeline;
|
||||
});
|
||||
@@ -97,7 +105,6 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === this._default)
|
||||
|
||||
@@ -26,10 +26,7 @@ import "./ha-sortable";
|
||||
|
||||
type ClockDatePartSection = "weekday" | "day" | "month" | "year" | "separator";
|
||||
|
||||
type ClockDateSeparatorPart = Extract<
|
||||
ClockCardDatePart,
|
||||
"separator-dash" | "separator-slash" | "separator-dot" | "separator-new-line"
|
||||
>;
|
||||
type ClockDateSeparatorPart = Extract<ClockCardDatePart, `separator-${string}`>;
|
||||
|
||||
const CLOCK_DATE_PART_SECTION_ORDER: readonly ClockDatePartSection[] = [
|
||||
"day",
|
||||
@@ -43,6 +40,7 @@ const CLOCK_DATE_SEPARATOR_VALUES: Record<ClockDateSeparatorPart, string> = {
|
||||
"separator-dash": "-",
|
||||
"separator-slash": "/",
|
||||
"separator-dot": ".",
|
||||
"separator-comma": ",",
|
||||
"separator-new-line": "",
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiCog } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { apiContext, configContext, entitiesContext } from "../data/context";
|
||||
import type { Agent } from "../data/conversation";
|
||||
import { listAgents } from "../data/conversation";
|
||||
import type { EntityConfigFlow } from "../dialogs/config-flow/entity-config-flow";
|
||||
@@ -11,7 +16,6 @@ import {
|
||||
fetchEntityConfigFlow,
|
||||
showEntityConfigFlow,
|
||||
} from "../dialogs/config-flow/entity-config-flow";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
|
||||
@@ -25,8 +29,6 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
@@ -35,6 +37,17 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
|
||||
@state() private _configFlow?: EntityConfigFlow;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
private _entities!: ContextType<typeof entitiesContext>;
|
||||
|
||||
protected render() {
|
||||
if (!this._agents) {
|
||||
return nothing;
|
||||
@@ -55,9 +68,7 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
if (!this.required) {
|
||||
options.unshift({
|
||||
value: NONE,
|
||||
label: this.hass.localize(
|
||||
"ui.components.conversation-agent-picker.none"
|
||||
),
|
||||
label: this._localize("ui.components.conversation-agent-picker.none"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,7 +76,7 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
<ha-select
|
||||
.label=${
|
||||
this.label ||
|
||||
this.hass!.localize(
|
||||
this._localize(
|
||||
"ui.components.conversation-agent-picker.conversation_agent"
|
||||
)
|
||||
}
|
||||
@@ -78,7 +89,7 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
>${
|
||||
this._configFlow
|
||||
? html`<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.components.conversation-agent-picker.settings"
|
||||
)}
|
||||
.path=${mdiCog}
|
||||
@@ -108,7 +119,7 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
let configFlow: EntityConfigFlow | undefined;
|
||||
try {
|
||||
configFlow = entityId
|
||||
? await fetchEntityConfigFlow(this.hass, entityId)
|
||||
? await fetchEntityConfigFlow(this._api, this._entities, entityId)
|
||||
: undefined;
|
||||
} catch (_err) {
|
||||
configFlow = undefined;
|
||||
@@ -122,9 +133,9 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
|
||||
private async _updateAgents() {
|
||||
const { agents } = await listAgents(
|
||||
this.hass,
|
||||
this._api.callWS,
|
||||
this.language,
|
||||
this.hass.config.country || undefined
|
||||
this._config.config.country || undefined
|
||||
);
|
||||
|
||||
this._agents = agents;
|
||||
@@ -184,7 +195,7 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
|
||||
private _openConfigFlow() {
|
||||
if (this._configFlow) {
|
||||
showEntityConfigFlow(this, this.hass, this._configFlow);
|
||||
showEntityConfigFlow(this, this._api, this._configFlow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +215,6 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -26,6 +28,10 @@ import "./chips/ha-input-chip";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import "./ha-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-input-helper-text";
|
||||
import type {
|
||||
PickerComboBoxItem,
|
||||
PickerComboBoxSearchFn,
|
||||
@@ -82,6 +88,8 @@ export class HaEntityTypePicker extends LitElement {
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states?: ContextType<typeof statesContext>;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
private _titlesLoaded = false;
|
||||
|
||||
protected willUpdate() {
|
||||
@@ -237,40 +245,13 @@ export class HaEntityTypePicker extends LitElement {
|
||||
const localize = this._i18n?.localize;
|
||||
const value = this.value ?? [];
|
||||
const chipLabels = localize ? this._chipLabels(localize, value) : undefined;
|
||||
|
||||
const addLabel = localize?.("ui.common.add");
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
${
|
||||
value.length && chipLabels
|
||||
? html`
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
value,
|
||||
(key) => key,
|
||||
(key) => {
|
||||
const label = chipLabels.get(key)!;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-key=${key}
|
||||
.label=${label}
|
||||
.disabled=${this.disabled}
|
||||
@remove=${this._removeItem}
|
||||
selected
|
||||
>
|
||||
${this._renderIcon(key, "icon")} ${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${this.label ? html`<label id="label">${this.label}</label>` : nothing}
|
||||
<ha-generic-picker
|
||||
.helper=${this.helper}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.value=${""}
|
||||
.addButtonLabel=${localize?.("ui.components.entity-type-picker.add")}
|
||||
.getItems=${this._getItems}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.searchFn=${this._searchFn}
|
||||
@@ -278,10 +259,53 @@ export class HaEntityTypePicker extends LitElement {
|
||||
.emptyLabel=${localize?.("ui.components.entity-type-picker.no_types")}
|
||||
no-sort
|
||||
@value-changed=${this._itemAdded}
|
||||
></ha-generic-picker>
|
||||
>
|
||||
<ha-chip-set
|
||||
slot="field"
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
${repeat(
|
||||
value,
|
||||
(key) => key,
|
||||
(key) => {
|
||||
const label = chipLabels?.get(key) ?? key;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-key=${key}
|
||||
.label=${label}
|
||||
.disabled=${this.disabled}
|
||||
@remove=${this._removeItem}
|
||||
selected
|
||||
>
|
||||
${this._renderIcon(key, "icon")} ${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
<ha-button
|
||||
size="s"
|
||||
appearance="filled"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._openPicker}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${addLabel}
|
||||
</ha-button>
|
||||
</ha-chip-set>
|
||||
</ha-generic-picker>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._picker?.open();
|
||||
}
|
||||
|
||||
private _itemAdded(ev: CustomEvent<{ value?: string }>) {
|
||||
ev.stopPropagation();
|
||||
const key = ev.detail.value;
|
||||
@@ -320,7 +344,15 @@ export class HaEntityTypePicker extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-2) 0;
|
||||
background-color: var(--mdc-text-field-fill-color);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-normal);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
ha-input-helper-text {
|
||||
padding: 0;
|
||||
margin-top: var(--ha-space-1);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -12,6 +11,11 @@ import {
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import type { Blueprints } from "../data/blueprint";
|
||||
@@ -19,11 +23,12 @@ import { fetchBlueprints } from "../data/blueprint";
|
||||
import { apiContext } from "../data/context";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
|
||||
@customElement("ha-filter-blueprints")
|
||||
export class HaFilterBlueprints extends LitElement {
|
||||
@@ -88,23 +93,30 @@ export class HaFilterBlueprints extends LitElement {
|
||||
${
|
||||
this._blueprints
|
||||
? html`
|
||||
<ha-list
|
||||
@selected=${this._blueprintsSelected}
|
||||
<ha-list-selectable
|
||||
multi
|
||||
class="ha-scrollbar"
|
||||
aria-label=${this._localize(
|
||||
"ui.panel.config.blueprint.caption"
|
||||
)}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${Object.entries(this._blueprints).map(
|
||||
([id, blueprint]) =>
|
||||
"error" in blueprint
|
||||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
: html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${id}
|
||||
.selected=${(this.value || []).includes(id)}
|
||||
.selected=${this.value?.includes(id) ?? false}
|
||||
>
|
||||
${blueprint.metadata.name || id}
|
||||
</ha-check-list-item>`
|
||||
<span slot="headline"
|
||||
>${blueprint.metadata.name || id}</span
|
||||
>
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
@@ -126,28 +138,20 @@ export class HaFilterBlueprints extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _blueprintsSelected(
|
||||
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
const blueprints = this._blueprints!;
|
||||
|
||||
if (!ev.detail.index.size) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
const blueprintId = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!blueprintId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
|
||||
for (const index of ev.detail.index) {
|
||||
const blueprintId = Object.keys(blueprints)[index];
|
||||
value.push(blueprintId);
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, blueprintId)
|
||||
: arrayWithout(this.value, blueprintId);
|
||||
}
|
||||
|
||||
private async _findRelated() {
|
||||
@@ -202,10 +206,14 @@ export class HaFilterBlueprints extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -35,12 +35,12 @@ import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showCategoryRegistryDetailDialog } from "../panels/config/category/show-dialog-category-registry-detail";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistantConnection } from "../types";
|
||||
import "./ha-button";
|
||||
import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-list-item";
|
||||
import "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
|
||||
@@ -226,10 +226,14 @@ export class HaFilterCategories extends LitElement {
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<ha-list-item graphic="icon" @click=${this._addCategory}>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon>
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
size="s"
|
||||
@click=${this._addCategory}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this._localize("ui.panel.config.category.editor.add")}
|
||||
</ha-list-item>
|
||||
</ha-button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
@@ -386,12 +390,12 @@ export class HaFilterCategories extends LitElement {
|
||||
ha-list-item-option {
|
||||
--ha-row-item-padding-block: var(--ha-space-2);
|
||||
}
|
||||
ha-button {
|
||||
margin: var(--ha-space-2) var(--ha-space-4);
|
||||
}
|
||||
ha-list-item-option ha-icon-button {
|
||||
--ha-icon-button-size: 32px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
ha-dropdown-item {
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
@@ -85,6 +85,15 @@ export class HaFilterDevices extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(properties: PropertyValues<this>) {
|
||||
super.updated(properties);
|
||||
|
||||
// The rows don't change with the value, so render the selection again
|
||||
if (properties.has("value")) {
|
||||
this._listElement?.requestRowsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
@@ -269,7 +278,6 @@ export class HaFilterDevices extends LitElement {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
this._listElement?.clearSelection();
|
||||
}
|
||||
|
||||
static styles = [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -13,17 +12,23 @@ import {
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, statesContext } from "../data/context";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@@ -82,35 +87,38 @@ export class HaFilterDomains extends LitElement {
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@selected=${this._handleItemSelected}
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label=${this._localize("ui.panel.config.domains.caption")}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${repeat(
|
||||
this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
this._filter
|
||||
),
|
||||
(i) => i,
|
||||
(domain) =>
|
||||
html`<ha-check-list-item
|
||||
html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${domain}
|
||||
.selected=${(this.value || []).includes(domain)}
|
||||
graphic="icon"
|
||||
.selected=${this.value?.includes(domain) ?? false}
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${domainToName(this._localize, domain)}
|
||||
</ha-check-list-item>`
|
||||
<span slot="headline"
|
||||
>${domainToName(this._localize, domain)}</span
|
||||
>
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
@@ -122,8 +130,7 @@ export class HaFilterDomains extends LitElement {
|
||||
states: ContextType<typeof statesContext>,
|
||||
localize: LocalizeFunc,
|
||||
language: string | undefined,
|
||||
filter: string | undefined,
|
||||
_value
|
||||
filter: string | undefined
|
||||
) => {
|
||||
const domains = new Set<string>();
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
@@ -150,22 +157,20 @@ export class HaFilterDomains extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleItemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
const domains = this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
);
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
const domain = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleDomains = new Set(domains);
|
||||
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
|
||||
const selected = [...ev.detail.index]
|
||||
.map((i) => domains[i])
|
||||
.filter((d): d is string => !!d);
|
||||
|
||||
this.value = [...preserved, ...selected];
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, domain)
|
||||
: arrayWithout(this.value, domain);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value.length ? this.value : undefined,
|
||||
@@ -189,10 +194,14 @@ export class HaFilterDomains extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -204,9 +213,6 @@ export class HaFilterDomains extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
@@ -25,16 +26,16 @@ import {
|
||||
} from "../data/context";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./ha-state-icon";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
import "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable-virtualized";
|
||||
import type { HaListSelectableVirtualized } from "./list/ha-list-selectable-virtualized";
|
||||
import type { HaListVirtualizedItem } from "./list/ha-list-virtualized";
|
||||
|
||||
interface EntityItem {
|
||||
interface EntityItem extends HaListVirtualizedItem {
|
||||
stateObj: HassEntity;
|
||||
name: string;
|
||||
}
|
||||
@@ -74,15 +75,14 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
|
||||
@query("ha-list-selectable-virtualized")
|
||||
private _list?: HaListSelectableVirtualized;
|
||||
|
||||
private _relatedRequestId = 0;
|
||||
|
||||
public willUpdate(properties: PropertyValues<this>) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("value") &&
|
||||
!deepEqual(this.value, properties.get("value"))
|
||||
@@ -91,6 +91,15 @@ export class HaFilterEntities extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(properties: PropertyValues<this>) {
|
||||
super.updated(properties);
|
||||
|
||||
// The rows don't change with the value, so render the selection again
|
||||
if (properties.has("value")) {
|
||||
this._list?.requestRowsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
@@ -121,23 +130,14 @@ export class HaFilterEntities extends LitElement {
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._entities(
|
||||
this._states,
|
||||
this._formatters,
|
||||
this.type,
|
||||
this._filter || "",
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${this._renderItem}
|
||||
@click=${this._handleItemClick}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</ha-list>
|
||||
<ha-list-selectable-virtualized
|
||||
multi
|
||||
aria-label=${this._localize("ui.panel.config.entities.caption")}
|
||||
.rows=${this._currentEntities()}
|
||||
.rowRenderer=${this._renderItem}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
></ha-list-selectable-virtualized>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
@@ -145,43 +145,43 @@ export class HaFilterEntities extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _keyFunction = (item?: EntityItem) => item?.stateObj.entity_id;
|
||||
|
||||
private _renderItem = (item?: EntityItem) =>
|
||||
!item
|
||||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
tabindex="0"
|
||||
.value=${item.stateObj.entity_id}
|
||||
.selected=${this.value?.includes(item.stateObj.entity_id) ?? false}
|
||||
graphic="icon"
|
||||
: html`<ha-list-item-option
|
||||
style="width: 100%;"
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${item.id}
|
||||
.selected=${this.value?.includes(item.id) ?? false}
|
||||
>
|
||||
<ha-state-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
></ha-state-icon>
|
||||
${item.name}
|
||||
</ha-check-list-item>`;
|
||||
<span slot="headline">${item.name}</span>
|
||||
</ha-list-item-option>`;
|
||||
|
||||
private _handleItemKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._handleItemClick(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
const listItem = ev.target.closest("ha-check-list-item");
|
||||
const value = listItem?.value;
|
||||
if (!value) {
|
||||
private _handleItemToggled(ev: CustomEvent<number>) {
|
||||
const entityId = this._currentEntities()[ev.detail]?.id;
|
||||
if (!entityId) {
|
||||
return;
|
||||
}
|
||||
if (this.value?.includes(value)) {
|
||||
this.value = this.value?.filter((val) => val !== value);
|
||||
} else {
|
||||
this.value = [...(this.value || []), value];
|
||||
}
|
||||
listItem.selected = this.value?.includes(value);
|
||||
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, entityId)
|
||||
: arrayWithout(this.value, entityId);
|
||||
}
|
||||
|
||||
private _currentEntities() {
|
||||
return this._entities(
|
||||
this._states,
|
||||
this._formatters,
|
||||
this.type,
|
||||
this._filter || "",
|
||||
this._i18n.locale.language
|
||||
);
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
@@ -199,12 +199,13 @@ export class HaFilterEntities extends LitElement {
|
||||
formatters: ContextType<typeof formattersContext>,
|
||||
type: this["type"],
|
||||
filter: string,
|
||||
language: string | undefined,
|
||||
_value
|
||||
language: string | undefined
|
||||
): EntityItem[] =>
|
||||
Object.values(states)
|
||||
.filter((stateObj) => !type || computeStateDomain(stateObj) !== type)
|
||||
.map((stateObj) => ({
|
||||
id: stateObj.entity_id,
|
||||
interactive: true,
|
||||
stateObj,
|
||||
name: formatters.formatEntityName(stateObj),
|
||||
}))
|
||||
@@ -267,10 +268,9 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable-virtualized {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -298,10 +298,6 @@ export class HaFilterEntities extends LitElement {
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
@@ -13,17 +12,23 @@ import {
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, manifestsContext } from "../data/context";
|
||||
import type { IntegrationManifest } from "../data/integration";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@@ -92,37 +97,41 @@ export class HaFilterIntegrations extends LitElement {
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@selected=${this._itemSelected}
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label=${this._localize(
|
||||
"ui.panel.config.integrations.caption"
|
||||
)}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${repeat(
|
||||
this._integrations(
|
||||
this._localize,
|
||||
manifests,
|
||||
this._filter,
|
||||
this.value,
|
||||
this._i18n.locale.language
|
||||
),
|
||||
(i) => i.domain,
|
||||
(integration) =>
|
||||
html`<ha-check-list-item
|
||||
html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${integration.domain}
|
||||
.selected=${(this.value || []).includes(
|
||||
integration.domain
|
||||
)}
|
||||
graphic="icon"
|
||||
.selected=${
|
||||
this.value?.includes(integration.domain) ??
|
||||
false
|
||||
}
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.domain=${integration.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${integration.name}
|
||||
</ha-check-list-item>`
|
||||
<span slot="headline">${integration.name}</span>
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>`
|
||||
</ha-list-selectable>`
|
||||
: nothing
|
||||
}
|
||||
</div>`
|
||||
@@ -144,7 +153,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
localize: LocalizeFunc,
|
||||
manifest: IntegrationManifest[],
|
||||
filter: string | undefined,
|
||||
_value: string[] | undefined,
|
||||
language: string
|
||||
) =>
|
||||
manifest
|
||||
@@ -165,26 +173,20 @@ export class HaFilterIntegrations extends LitElement {
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
);
|
||||
|
||||
private _itemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
if (!this._manifests) {
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
const domain = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
const integrations = this._integrations(
|
||||
this._localize,
|
||||
this._manifestList(this._manifests),
|
||||
this._filter,
|
||||
this.value,
|
||||
this._i18n.locale.language
|
||||
);
|
||||
|
||||
const visibleDomains = new Set(integrations.map((i) => i.domain));
|
||||
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
|
||||
const selected = [...ev.detail.index]
|
||||
.map((i) => integrations[i]?.domain)
|
||||
.filter((d): d is string => !!d);
|
||||
|
||||
this.value = [...preserved, ...selected];
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, domain)
|
||||
: arrayWithout(this.value, domain);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value.length ? this.value : undefined,
|
||||
@@ -208,10 +210,14 @@ export class HaFilterIntegrations extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -223,9 +229,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -13,22 +12,29 @@ import {
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, labelsContext } from "../data/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-label";
|
||||
import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
import "./item/ha-list-item-button";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-base";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
|
||||
@customElement("ha-filter-labels")
|
||||
export class HaFilterLabels extends LitElement {
|
||||
@@ -57,12 +63,10 @@ export class HaFilterLabels extends LitElement {
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
|
||||
private _filteredLabels = memoizeOne(
|
||||
// `_value` used to recalculate the memoization when the selection changes
|
||||
(
|
||||
labels: LabelRegistryEntry[],
|
||||
filter: string | undefined,
|
||||
language: string | undefined,
|
||||
_value
|
||||
language: string | undefined
|
||||
) =>
|
||||
labels
|
||||
.filter(
|
||||
@@ -105,26 +109,28 @@ export class HaFilterLabels extends LitElement {
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
@selected=${this._labelSelected}
|
||||
class="ha-scrollbar"
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label=${this._localize("ui.panel.config.labels.caption")}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${repeat(
|
||||
this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
this._i18n.locale.language
|
||||
),
|
||||
(label) => label.label_id,
|
||||
(label) =>
|
||||
html`<ha-check-list-item
|
||||
html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${label.label_id}
|
||||
.selected=${(this.value || []).includes(label.label_id)}
|
||||
hasMeta
|
||||
.selected=${this.value?.includes(label.label_id) ?? false}
|
||||
>
|
||||
<ha-label
|
||||
slot="headline"
|
||||
.color=${label.color}
|
||||
.description=${label.description}
|
||||
>
|
||||
@@ -138,13 +144,17 @@ export class HaFilterLabels extends LitElement {
|
||||
}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-check-list-item>`
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>
|
||||
<ha-list-item graphic="icon" @click=${this._manageLabels}>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
${this._localize("ui.panel.config.labels.manage_labels")}
|
||||
</ha-list-item>
|
||||
</ha-list-selectable>
|
||||
<ha-list-base>
|
||||
<ha-list-item-button @click=${this._manageLabels}>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
<span slot="headline"
|
||||
>${this._localize("ui.panel.config.labels.manage_labels")}</span
|
||||
>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
@@ -164,35 +174,23 @@ export class HaFilterLabels extends LitElement {
|
||||
this._filter = value.toLowerCase();
|
||||
}
|
||||
|
||||
private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
const filteredLabels = this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
);
|
||||
|
||||
const filteredLabelIds = new Set(filteredLabels.map((l) => l.label_id));
|
||||
|
||||
// Keep previously selected labels that are not in the current filtered view
|
||||
const preservedLabels = (this.value || []).filter(
|
||||
(id) => !filteredLabelIds.has(id)
|
||||
);
|
||||
|
||||
// Build the new selection from the filtered labels based on selected indices
|
||||
const newlySelectedLabels: string[] = [];
|
||||
for (const index of ev.detail.index) {
|
||||
const labelId = filteredLabels[index]?.label_id;
|
||||
if (labelId) {
|
||||
newlySelectedLabels.push(labelId);
|
||||
}
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
const labelId = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!labelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = [...preservedLabels, ...newlySelectedLabels];
|
||||
this.value = value.length ? value : [];
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, labelId)
|
||||
: arrayWithout(this.value, labelId);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: value.length ? value : undefined,
|
||||
value: this.value.length ? this.value : undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
@@ -208,10 +206,14 @@ export class HaFilterLabels extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
|
||||
@customElement("ha-filter-states")
|
||||
export class HaFilterStates extends LitElement {
|
||||
@@ -40,7 +46,6 @@ export class HaFilterStates extends LitElement {
|
||||
if (!this.states) {
|
||||
return nothing;
|
||||
}
|
||||
const hasIcon = this.states.find((item) => item.icon);
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
@@ -64,30 +69,32 @@ export class HaFilterStates extends LitElement {
|
||||
this._panel.showContent
|
||||
? html`
|
||||
<div class="content" ${ref(this._content)}>
|
||||
<ha-list
|
||||
@selected=${this._statesSelected}
|
||||
<ha-list-selectable
|
||||
multi
|
||||
class="ha-scrollbar"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${this.states.map(
|
||||
(item) =>
|
||||
html`<ha-check-list-item
|
||||
html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${item.value}
|
||||
.selected=${this.value?.includes(item.value) ?? false}
|
||||
.graphic=${hasIcon ? "icon" : null}
|
||||
>
|
||||
${
|
||||
item.icon
|
||||
? html`<ha-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.icon=${item.icon}
|
||||
></ha-icon>`
|
||||
: nothing
|
||||
}
|
||||
${item.label}
|
||||
</ha-check-list-item>`
|
||||
<span slot="headline">${item.label}</span>
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
@@ -99,26 +106,23 @@ export class HaFilterStates extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _statesSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
if (!ev.detail.index.size) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
const state = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
|
||||
for (const index of ev.detail.index) {
|
||||
const val = this.states![index].value;
|
||||
value.push(val);
|
||||
}
|
||||
this.value = value;
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, state)
|
||||
: arrayWithout(this.value, state);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value,
|
||||
value: this.value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
@@ -134,10 +138,14 @@ export class HaFilterStates extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -11,15 +10,19 @@ import {
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { arrayWith, arrayWithout } from "../common/array/array-with";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-label";
|
||||
import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
import "./voice-assistant-brand-icon";
|
||||
import { voiceAssistants } from "../data/expose";
|
||||
|
||||
@@ -65,30 +68,35 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`<div class="content" ${ref(this._content)}>
|
||||
<ha-list
|
||||
@selected=${this._assistantsSelected}
|
||||
class="ha-scrollbar"
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label=${this._localize(
|
||||
"ui.panel.config.dashboard.voice_assistants.main"
|
||||
)}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${repeat(
|
||||
this._voiceAssistantOptions,
|
||||
(voiceAssistantId) => voiceAssistantId,
|
||||
(voiceAssistantId) =>
|
||||
html`<ha-check-list-item
|
||||
html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${voiceAssistantId}
|
||||
.selected=${(this.value || []).includes(voiceAssistantId)}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
.selected=${this.value?.includes(voiceAssistantId) ?? false}
|
||||
>
|
||||
<voice-assistant-brand-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.voiceAssistantId=${voiceAssistantId}
|
||||
>
|
||||
</voice-assistant-brand-icon>
|
||||
${voiceAssistants[voiceAssistantId].name}
|
||||
</ha-check-list-item>`
|
||||
<span slot="headline"
|
||||
>${voiceAssistants[voiceAssistantId].name}</span
|
||||
>
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-selectable>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
@@ -104,23 +112,20 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _assistantsSelected(
|
||||
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||
private _handleItemToggled(
|
||||
ev: HASSDomEvent<number> & HASSDomCurrentTargetEvent<HaListSelectable>
|
||||
) {
|
||||
if (!ev.detail.index) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
const voiceAssistantId = (
|
||||
ev.currentTarget.items[ev.detail] as HaListItemOption | undefined
|
||||
)?.value;
|
||||
if (!voiceAssistantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newvalue: string[] = [];
|
||||
for (const index of ev.detail.index) {
|
||||
newvalue.push(this._voiceAssistantOptions![index]);
|
||||
}
|
||||
this.value = newvalue;
|
||||
this.value =
|
||||
ev.type === "ha-list-item-selected"
|
||||
? arrayWith(this.value, voiceAssistantId)
|
||||
: arrayWithout(this.value, voiceAssistantId);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value,
|
||||
@@ -139,10 +144,14 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -154,9 +163,6 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
@@ -5,10 +6,20 @@ import { LitElement, html } from "lit";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { updateAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import {
|
||||
apiContext,
|
||||
areasContext,
|
||||
devicesContext,
|
||||
entitiesContext,
|
||||
floorsContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import type {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
@@ -22,7 +33,7 @@ import {
|
||||
} from "../data/floor_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-floor-icon";
|
||||
@@ -47,8 +58,6 @@ interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||
|
||||
@customElement("ha-floor-picker")
|
||||
export class HaFloorPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -102,16 +111,38 @@ export class HaFloorPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
private _entities!: ContextType<typeof entitiesContext>;
|
||||
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
private _devices!: ContextType<typeof devicesContext>;
|
||||
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
private _floors!: ContextType<typeof floorsContext>;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@state() private _pendingFloorId?: string;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (
|
||||
this._pendingFloorId &&
|
||||
changedProperties.has("hass") &&
|
||||
this.hass.floors !== changedProperties.get("hass")?.floors &&
|
||||
this.hass.floors[this._pendingFloorId]
|
||||
changedProperties.has("_floors") &&
|
||||
this._floors[this._pendingFloorId]
|
||||
) {
|
||||
this._setValue(this._pendingFloorId);
|
||||
this._pendingFloorId = undefined;
|
||||
@@ -123,11 +154,11 @@ export class HaFloorPicker extends LitElement {
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
// Recompute value renderer when the areas change
|
||||
// Recompute value renderer when the floors change
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
(_haAreas: HomeAssistant["floors"]): PickerValueRenderer =>
|
||||
(haFloors: ContextType<typeof floorsContext>): PickerValueRenderer =>
|
||||
(value) => {
|
||||
const floor = this.hass.floors[value];
|
||||
const floor = haFloors[value];
|
||||
|
||||
if (!floor) {
|
||||
return html`
|
||||
@@ -147,10 +178,11 @@ export class HaFloorPicker extends LitElement {
|
||||
|
||||
private _getFloors = memoizeOne(
|
||||
(
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
haFloors: ContextType<typeof floorsContext>,
|
||||
haAreas: ContextType<typeof areasContext>,
|
||||
haDevices: ContextType<typeof devicesContext>,
|
||||
haEntities: ContextType<typeof entitiesContext>,
|
||||
haStates: ContextType<typeof statesContext>,
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
@@ -217,7 +249,7 @@ export class HaFloorPicker extends LitElement {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -228,7 +260,7 @@ export class HaFloorPicker extends LitElement {
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
@@ -249,7 +281,7 @@ export class HaFloorPicker extends LitElement {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -257,7 +289,7 @@ export class HaFloorPicker extends LitElement {
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -342,10 +374,11 @@ export class HaFloorPicker extends LitElement {
|
||||
|
||||
private _getItems = () =>
|
||||
this._getFloors(
|
||||
this.hass.floors,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._floors,
|
||||
this._areas,
|
||||
this._devices,
|
||||
this._entities,
|
||||
this._states,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
@@ -355,7 +388,7 @@ export class HaFloorPicker extends LitElement {
|
||||
);
|
||||
|
||||
private _allFloorNames = memoizeOne(
|
||||
(floors: HomeAssistant["floors"]) =>
|
||||
(floors: ContextType<typeof floorsContext>) =>
|
||||
Object.values(floors)
|
||||
.map((floor) => computeFloorName(floor)?.toLowerCase())
|
||||
.filter(Boolean) as string[]
|
||||
@@ -368,13 +401,13 @@ export class HaFloorPicker extends LitElement {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allFloors = this._allFloorNames(this.hass.floors);
|
||||
const allFloors = this._allFloorNames(this._floors);
|
||||
|
||||
if (searchString && !allFloors.includes(searchString.toLowerCase())) {
|
||||
return [
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
primary: this._i18n.localize(
|
||||
"ui.components.floor-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
@@ -388,7 +421,7 @@ export class HaFloorPicker extends LitElement {
|
||||
return [
|
||||
{
|
||||
id: ADD_NEW_ID,
|
||||
primary: this.hass.localize("ui.components.floor-picker.add_new"),
|
||||
primary: this._i18n.localize("ui.components.floor-picker.add_new"),
|
||||
icon_path: mdiPlus,
|
||||
},
|
||||
];
|
||||
@@ -397,9 +430,9 @@ export class HaFloorPicker extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.floor-picker.floor");
|
||||
this._i18n.localize("ui.components.floor-picker.floor");
|
||||
|
||||
const valueRenderer = this._computeValueRenderer(this.hass.floors);
|
||||
const valueRenderer = this._computeValueRenderer(this._floors);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
@@ -409,7 +442,7 @@ export class HaFloorPicker extends LitElement {
|
||||
.helper=${this.helper}
|
||||
.placeholder=${placeholder}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
.emptyLabel=${this._i18n.localize(
|
||||
"ui.components.floor-picker.no_floors"
|
||||
)}
|
||||
.value=${this.value}
|
||||
@@ -418,7 +451,7 @@ export class HaFloorPicker extends LitElement {
|
||||
.valueRenderer=${valueRenderer}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
.unknownItemText=${this._i18n.localize(
|
||||
"ui.components.floor-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -437,7 +470,7 @@ export class HaFloorPicker extends LitElement {
|
||||
}
|
||||
|
||||
if (value.startsWith(ADD_NEW_ID)) {
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
this._i18n.loadFragmentTranslation("config");
|
||||
|
||||
const suggestedName = value.substring(ADD_NEW_ID.length);
|
||||
|
||||
@@ -445,20 +478,23 @@ export class HaFloorPicker extends LitElement {
|
||||
suggestedName: suggestedName,
|
||||
createEntry: async (values, addedAreas) => {
|
||||
try {
|
||||
const floor = await createFloorRegistryEntry(this.hass, values);
|
||||
const floor = await createFloorRegistryEntry(
|
||||
this._api.callWS,
|
||||
values
|
||||
);
|
||||
addedAreas.forEach((areaId) => {
|
||||
updateAreaRegistryEntry(this.hass, areaId, {
|
||||
updateAreaRegistryEntry(this._api.callWS, areaId, {
|
||||
floor_id: floor.floor_id,
|
||||
});
|
||||
});
|
||||
if (this.hass.floors[floor.floor_id]) {
|
||||
if (this._floors[floor.floor_id]) {
|
||||
this._setValue(floor.floor_id);
|
||||
} else {
|
||||
this._pendingFloorId = floor.floor_id;
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
title: this._i18n.localize(
|
||||
"ui.components.floor-picker.failed_create_floor"
|
||||
),
|
||||
text: err.message,
|
||||
@@ -479,7 +515,7 @@ export class HaFloorPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.floor-picker.no_match", {
|
||||
this._i18n.localize("ui.components.floor-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-floor-picker";
|
||||
|
||||
@customElement("ha-floors-picker")
|
||||
export class HaFloorsPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
export class HaFloorsPicker extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
@@ -63,10 +59,6 @@ export class HaFloorsPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const currentFloors = this._currentFloors;
|
||||
return html`
|
||||
${currentFloors.map(
|
||||
@@ -75,7 +67,6 @@ export class HaFloorsPicker extends SubscribeMixin(LitElement) {
|
||||
<ha-floor-picker
|
||||
.curValue=${floor}
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.value=${floor}
|
||||
.label=${this.pickedFloorLabel}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@@ -92,7 +83,6 @@ export class HaFloorsPicker extends SubscribeMixin(LitElement) {
|
||||
<div>
|
||||
<ha-floor-picker
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.label=${this.pickFloorLabel}
|
||||
.helper=${this.helper}
|
||||
.includeDomains=${this.includeDomains}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
@@ -15,7 +16,13 @@ import memoizeOne from "memoize-one";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { labelsContext } from "../data/context";
|
||||
import {
|
||||
apiContext,
|
||||
internationalizationContext,
|
||||
labelsContext,
|
||||
registriesContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import {
|
||||
getLabels,
|
||||
labelComboBoxKeys,
|
||||
@@ -27,7 +34,7 @@ import {
|
||||
} from "../data/label/label_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
@@ -53,8 +60,6 @@ export const renderLabelColorBadge = (color: string | undefined) =>
|
||||
|
||||
@customElement("ha-label-picker")
|
||||
export class HaLabelPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -112,6 +117,19 @@ export class HaLabelPicker extends LitElement {
|
||||
@state()
|
||||
private _labels?: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states?: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries?: ContextType<typeof registriesContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@queryAssignedElements({ flatten: true })
|
||||
private _slotNodes?: NodeListOf<HTMLElement>;
|
||||
|
||||
@@ -149,12 +167,16 @@ export class HaLabelPicker extends LitElement {
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getLabelsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
private _getItems = () => {
|
||||
const { areas, devices, entities } = this._registries ?? {};
|
||||
if (!this._states || !areas || !devices || !entities) {
|
||||
return [];
|
||||
}
|
||||
return this._getLabelsMemoized(
|
||||
this._states,
|
||||
areas,
|
||||
devices,
|
||||
entities,
|
||||
this._labels,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
@@ -163,6 +185,7 @@ export class HaLabelPicker extends LitElement {
|
||||
this.entityFilter,
|
||||
this.excludeLabels
|
||||
);
|
||||
};
|
||||
|
||||
private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
|
||||
if (!labels) {
|
||||
@@ -190,7 +213,7 @@ export class HaLabelPicker extends LitElement {
|
||||
return [
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
primary: this._i18n.localize(
|
||||
"ui.components.label-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
@@ -204,7 +227,7 @@ export class HaLabelPicker extends LitElement {
|
||||
return [
|
||||
{
|
||||
id: ADD_NEW_ID,
|
||||
primary: this.hass.localize("ui.components.label-picker.add_new"),
|
||||
primary: this._i18n.localize("ui.components.label-picker.add_new"),
|
||||
icon_path: mdiPlus,
|
||||
},
|
||||
];
|
||||
@@ -213,7 +236,7 @@ export class HaLabelPicker extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.label-picker.label");
|
||||
this._i18n.localize("ui.components.label-picker.label");
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
@@ -222,10 +245,10 @@ export class HaLabelPicker extends LitElement {
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize(
|
||||
.emptyLabel=${this._i18n.localize(
|
||||
"ui.components.label-picker.no_labels"
|
||||
)}
|
||||
.addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
|
||||
.addButtonLabel=${this._i18n.localize("ui.components.label-picker.add")}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
@@ -253,7 +276,7 @@ export class HaLabelPicker extends LitElement {
|
||||
}
|
||||
|
||||
if (value.startsWith(ADD_NEW_ID)) {
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
this._i18n.loadFragmentTranslation("config");
|
||||
|
||||
const suggestedName = value.substring(ADD_NEW_ID.length);
|
||||
|
||||
@@ -261,7 +284,10 @@ export class HaLabelPicker extends LitElement {
|
||||
suggestedName: suggestedName,
|
||||
createEntry: async (values) => {
|
||||
try {
|
||||
const label = await createLabelRegistryEntry(this.hass, values);
|
||||
const label = await createLabelRegistryEntry(
|
||||
this._api.callWS,
|
||||
values
|
||||
);
|
||||
if (this._labels?.some((l) => l.label_id === label.label_id)) {
|
||||
this._setValue(label.label_id);
|
||||
} else {
|
||||
@@ -269,7 +295,7 @@ export class HaLabelPicker extends LitElement {
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
title: this._i18n.localize(
|
||||
"ui.components.label-picker.failed_create_label"
|
||||
),
|
||||
text: err.message,
|
||||
@@ -292,7 +318,7 @@ export class HaLabelPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.label-picker.no_match", {
|
||||
this._i18n.localize("ui.components.label-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
@@ -8,11 +9,15 @@ import memoizeOne from "memoize-one";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { labelsContext } from "../data/context";
|
||||
import {
|
||||
apiContext,
|
||||
internationalizationContext,
|
||||
labelsContext,
|
||||
} from "../data/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { updateLabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./chips/ha-chip-set";
|
||||
import "./chips/ha-input-chip";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
@@ -23,8 +28,6 @@ import "./ha-tooltip";
|
||||
|
||||
@customElement("ha-labels-picker")
|
||||
export class HaLabelsPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
@@ -82,6 +85,13 @@ export class HaLabelsPicker extends LitElement {
|
||||
@state()
|
||||
private _labels?: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@query("ha-label-picker", true) public labelPicker!: HaLabelPicker;
|
||||
|
||||
public async open() {
|
||||
@@ -114,12 +124,11 @@ export class HaLabelsPicker extends LitElement {
|
||||
const labels = this._sortedLabels(
|
||||
this.value,
|
||||
this._labels,
|
||||
this.hass.locale.language
|
||||
this._i18n.locale.language
|
||||
);
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-label-picker
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@@ -175,7 +184,7 @@ export class HaLabelsPicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlaylistPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize("ui.components.label-picker.add")}
|
||||
${this._i18n.localize("ui.components.label-picker.add")}
|
||||
</ha-button>
|
||||
</ha-chip-set>
|
||||
</ha-label-picker>
|
||||
@@ -196,7 +205,11 @@ export class HaLabelsPicker extends LitElement {
|
||||
showLabelDetailDialog(this, {
|
||||
entry: label,
|
||||
updateEntry: async (values) => {
|
||||
await updateLabelRegistryEntry(this.hass, label.label_id, values);
|
||||
await updateLabelRegistryEntry(
|
||||
this._api.callWS,
|
||||
label.label_id,
|
||||
values
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -166,7 +166,6 @@ export class HaMediaItemPicker extends LitElement {
|
||||
image"
|
||||
>
|
||||
<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${this.value.metadata.thumbnail}
|
||||
></ha-media-browser-thumbnail>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiBackupRestore, mdiFolder, mdiHarddisk, mdiPlayBox } from "@mdi/js";
|
||||
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 { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import {
|
||||
apiContext,
|
||||
configContext,
|
||||
internationalizationContext,
|
||||
} from "../data/context";
|
||||
import type { SupervisorMounts } from "../data/supervisor/mounts";
|
||||
import {
|
||||
fetchSupervisorMounts,
|
||||
@@ -13,9 +20,7 @@ import {
|
||||
SupervisorMountUsage,
|
||||
supervisorMountDescription,
|
||||
} from "../data/supervisor/mounts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
|
||||
@@ -23,8 +28,6 @@ const _BACKUP_DATA_DISK_ = "/backup";
|
||||
|
||||
@customElement("ha-mount-picker")
|
||||
class HaMountPicker extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -41,6 +44,16 @@ class HaMountPicker extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._getMounts();
|
||||
}
|
||||
@@ -55,7 +68,8 @@ class HaMountPicker extends LitElement {
|
||||
|
||||
const options: HaSelectOption[] = this._filterMounts(
|
||||
this._mounts,
|
||||
this.usage
|
||||
this.usage,
|
||||
this._i18n.locale.language
|
||||
).map((mount) => ({
|
||||
value: mount.name,
|
||||
label: mount.name,
|
||||
@@ -73,7 +87,7 @@ class HaMountPicker extends LitElement {
|
||||
value: _BACKUP_DATA_DISK_,
|
||||
iconPath: mdiHarddisk,
|
||||
label:
|
||||
this.hass.localize("ui.components.mount-picker.use_datadisk") ||
|
||||
this._i18n.localize("ui.components.mount-picker.use_datadisk") ||
|
||||
"Use data disk for backup",
|
||||
};
|
||||
if (
|
||||
@@ -89,8 +103,8 @@ class HaMountPicker extends LitElement {
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${
|
||||
this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.mount-picker.mount")
|
||||
this.label === undefined
|
||||
? this._i18n.localize("ui.components.mount-picker.mount")
|
||||
: this.label
|
||||
}
|
||||
.value=${this.value}
|
||||
@@ -105,7 +119,7 @@ class HaMountPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _filterMounts = memoizeOne(
|
||||
(mounts: SupervisorMounts, usage: this["usage"]) => {
|
||||
(mounts: SupervisorMounts, usage: this["usage"], language: string) => {
|
||||
let filteredMounts = mounts.mounts.filter((mount) =>
|
||||
[
|
||||
SupervisorMountType.CIFS,
|
||||
@@ -125,29 +139,25 @@ class HaMountPicker extends LitElement {
|
||||
if (mountB.name === mounts.default_backup_mount) {
|
||||
return 1;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
mountA.name,
|
||||
mountB.name,
|
||||
this.hass.locale.language
|
||||
);
|
||||
return caseInsensitiveStringCompare(mountA.name, mountB.name, language);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
private async _getMounts() {
|
||||
try {
|
||||
if (isComponentLoaded(this.hass.config, "hassio")) {
|
||||
this._mounts = await fetchSupervisorMounts(this.hass);
|
||||
if (isComponentLoaded(this._config.config, "hassio")) {
|
||||
this._mounts = await fetchSupervisorMounts(this._api.callWS);
|
||||
if (this.usage === SupervisorMountUsage.BACKUP && !this.value) {
|
||||
this.value = this._mounts.default_backup_mount || _BACKUP_DATA_DISK_;
|
||||
}
|
||||
} else {
|
||||
this._error = this.hass.localize(
|
||||
this._error = this._i18n.localize(
|
||||
"ui.components.mount-picker.error.no_supervisor"
|
||||
);
|
||||
}
|
||||
} catch (_err: any) {
|
||||
this._error = this.hass.localize(
|
||||
this._error = this._i18n.localize(
|
||||
"ui.components.mount-picker.error.fetch_mounts"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { subscribeOneCollection } from "../common/util/subscribe-one";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
@@ -13,12 +15,30 @@ import { SYSTEM_PANELS } from "../data/panel";
|
||||
import {
|
||||
CONFIG_SUB_ROUTES,
|
||||
computeNavigationPathInfo,
|
||||
type NavigationPathInfoOptions,
|
||||
} from "../data/compute-navigation-path-info";
|
||||
import {
|
||||
apiContext,
|
||||
configContext,
|
||||
connectionContext,
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
uiContext,
|
||||
} from "../data/context";
|
||||
import { findRelated, type RelatedResult } from "../data/search";
|
||||
import { PANEL_DASHBOARDS } from "../panels/config/lovelace/dashboards/ha-config-lovelace-dashboards";
|
||||
import { computeAreaPath } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { multiTermSortedSearch } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type {
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantConnection,
|
||||
HomeAssistantInternationalization,
|
||||
HomeAssistantRegistries,
|
||||
HomeAssistantUI,
|
||||
Panels,
|
||||
ValueChangedEvent,
|
||||
} from "../types";
|
||||
import type { ActionRelatedContext } from "../panels/lovelace/components/hui-action-editor";
|
||||
import "./ha-generic-picker";
|
||||
import "./ha-domain-icon";
|
||||
@@ -50,8 +70,6 @@ interface NavigationItem extends PickerComboBoxItem {
|
||||
|
||||
@customElement("ha-navigation-picker")
|
||||
export class HaNavigationPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -70,6 +88,28 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public context?: ActionRelatedContext;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
@transform<HomeAssistantUI, Panels | undefined>({
|
||||
transformer: (ui) => ui?.panels ?? undefined,
|
||||
})
|
||||
private _panels?: Panels;
|
||||
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries?: HomeAssistantRegistries;
|
||||
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig?: HomeAssistantConfig;
|
||||
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: HomeAssistantConnection;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api?: HomeAssistantApi;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._loadNavigationItems();
|
||||
}
|
||||
@@ -98,7 +138,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
? [
|
||||
{
|
||||
id: "related",
|
||||
label: this.hass.localize(
|
||||
label: this._i18n.localize(
|
||||
"ui.components.navigation-picker.related"
|
||||
),
|
||||
},
|
||||
@@ -106,23 +146,27 @@ export class HaNavigationPicker extends LitElement {
|
||||
: []),
|
||||
{
|
||||
id: "dashboards",
|
||||
label: this.hass.localize("ui.components.navigation-picker.dashboards"),
|
||||
label: this._i18n.localize(
|
||||
"ui.components.navigation-picker.dashboards"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "views",
|
||||
label: this.hass.localize("ui.components.navigation-picker.views"),
|
||||
label: this._i18n.localize("ui.components.navigation-picker.views"),
|
||||
},
|
||||
...(this._navigationGroups.apps.length
|
||||
? [
|
||||
{
|
||||
id: "apps",
|
||||
label: this.hass.localize("ui.components.navigation-picker.apps"),
|
||||
label: this._i18n.localize(
|
||||
"ui.components.navigation-picker.apps"
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "other_routes",
|
||||
label: this.hass.localize(
|
||||
label: this._i18n.localize(
|
||||
"ui.components.navigation-picker.other_routes"
|
||||
),
|
||||
},
|
||||
@@ -140,7 +184,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.sections=${sections}
|
||||
.customValueLabel=${this.hass.localize(
|
||||
.customValueLabel=${this._i18n.localize(
|
||||
"ui.components.navigation-picker.add_custom_path"
|
||||
)}
|
||||
.addButtonLabel=${this.addButtonLabel}
|
||||
@@ -268,7 +312,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
}
|
||||
if (!section && groupItems.length) {
|
||||
items.push(
|
||||
this.hass.localize(`ui.components.navigation-picker.${group}`)
|
||||
this._i18n.localize(`ui.components.navigation-picker.${group}`)
|
||||
);
|
||||
}
|
||||
items.push(...groupItems);
|
||||
@@ -290,22 +334,25 @@ export class HaNavigationPicker extends LitElement {
|
||||
caseInsensitiveStringCompare(
|
||||
itemA.sorting_label!,
|
||||
itemB.sorting_label!,
|
||||
this.hass.locale.language
|
||||
this._i18n.locale.language
|
||||
);
|
||||
|
||||
private async _loadNavigationItems() {
|
||||
await this._loadConfigEntries();
|
||||
const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({
|
||||
id,
|
||||
...panel,
|
||||
}));
|
||||
const pathInfoOptions = this._pathInfoOptions();
|
||||
const panels = Object.entries(pathInfoOptions.panels).map(
|
||||
([id, panel]) => ({
|
||||
id,
|
||||
...panel,
|
||||
})
|
||||
);
|
||||
const lovelacePanels = panels.filter(
|
||||
(panel) => panel.component_name === "lovelace"
|
||||
);
|
||||
|
||||
const viewConfigs = await Promise.all(
|
||||
lovelacePanels.map((panel) =>
|
||||
fetchConfig(this.hass!.connection, panel.url_path, true)
|
||||
fetchConfig(this._connection!.connection, panel.url_path, true)
|
||||
.then((config) => [panel.id, config] as [string, typeof config])
|
||||
.catch((_) => [panel.id, undefined] as [string, undefined])
|
||||
)
|
||||
@@ -324,7 +371,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
// Skip app panels — they are handled by the ingress panels fetch below
|
||||
if (panel.component_name === "app") continue;
|
||||
const path = `/${panel.url_path}`;
|
||||
const resolved = computeNavigationPathInfo(this.hass!, path);
|
||||
const resolved = computeNavigationPathInfo(pathInfoOptions, path);
|
||||
const isDashboardPanel =
|
||||
panel.component_name === "lovelace" ||
|
||||
PANEL_DASHBOARDS.includes(panel.id);
|
||||
@@ -350,7 +397,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
config.views.forEach((view, index) => {
|
||||
const viewPath = `/${panel.url_path}/${view.path ?? index}`;
|
||||
const viewInfo = computeNavigationPathInfo(
|
||||
this.hass!,
|
||||
pathInfoOptions,
|
||||
viewPath,
|
||||
config
|
||||
);
|
||||
@@ -366,15 +413,18 @@ export class HaNavigationPicker extends LitElement {
|
||||
}
|
||||
|
||||
// Fetch all ingress add-on panels
|
||||
if (isComponentLoaded(this.hass!.config, "hassio")) {
|
||||
if (
|
||||
this._hassConfig?.config &&
|
||||
isComponentLoaded(this._hassConfig.config, "hassio")
|
||||
) {
|
||||
try {
|
||||
const ingressPanels = await subscribeOneCollection(
|
||||
getIngressPanelInfoCollection(this.hass!.connection)
|
||||
getIngressPanelInfoCollection(this._connection!.connection)
|
||||
);
|
||||
for (const slug of Object.keys(ingressPanels)) {
|
||||
const path = `/app/${slug}`;
|
||||
const resolved = computeNavigationPathInfo(
|
||||
this.hass!,
|
||||
pathInfoOptions,
|
||||
path,
|
||||
undefined,
|
||||
ingressPanels
|
||||
@@ -396,7 +446,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
for (const [subPath, route] of Object.entries(CONFIG_SUB_ROUTES)) {
|
||||
const path = `/config/${subPath}`;
|
||||
const label = this.hass!.localize(route.translationKey) || subPath;
|
||||
const label = this._i18n.localize(route.translationKey) || subPath;
|
||||
otherRoutes.push({
|
||||
id: path,
|
||||
primary: label,
|
||||
@@ -445,9 +495,10 @@ export class HaNavigationPicker extends LitElement {
|
||||
...this._navigationGroups.apps,
|
||||
...this._navigationGroups.other_routes,
|
||||
];
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
if (!this.hass || (!this.context?.entity_id && !this.context?.area_id)) {
|
||||
if (!this.context?.entity_id && !this.context?.area_id) {
|
||||
updateRelatedItems([]);
|
||||
return;
|
||||
}
|
||||
@@ -472,8 +523,8 @@ export class HaNavigationPicker extends LitElement {
|
||||
let relatedResult: RelatedResult | undefined;
|
||||
try {
|
||||
relatedResult = context.entity_id
|
||||
? await findRelated(this.hass, "entity", context.entity_id)
|
||||
: await findRelated(this.hass, "area", context.area_id!);
|
||||
? await findRelated(this._api!, "entity", context.entity_id)
|
||||
: await findRelated(this._api!, "area", context.area_id!);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching related items for navigation picker", err);
|
||||
@@ -486,11 +537,12 @@ export class HaNavigationPicker extends LitElement {
|
||||
relatedAreaIds.add(context.area_id);
|
||||
}
|
||||
|
||||
const pathInfoOptions = this._pathInfoOptions();
|
||||
const relatedItems: NavigationItem[] = [];
|
||||
for (const deviceId of relatedDeviceIds) {
|
||||
const device = this.hass.devices[deviceId];
|
||||
const device = pathInfoOptions.devices[deviceId];
|
||||
const path = `/config/devices/device/${deviceId}`;
|
||||
const resolved = computeNavigationPathInfo(this.hass, path);
|
||||
const resolved = computeNavigationPathInfo(pathInfoOptions, path);
|
||||
relatedItems.push({
|
||||
id: path,
|
||||
primary: resolved.label,
|
||||
@@ -512,7 +564,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
for (const areaId of relatedAreaIds) {
|
||||
// Area dashboard view
|
||||
const viewPath = `/home/${computeAreaPath(areaId)}`;
|
||||
const resolvedArea = computeNavigationPathInfo(this.hass, viewPath);
|
||||
const resolvedArea = computeNavigationPathInfo(pathInfoOptions, viewPath);
|
||||
relatedItems.push({
|
||||
id: viewPath,
|
||||
primary: resolvedArea.label,
|
||||
@@ -531,7 +583,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
const path = `/config/areas/area/${areaId}`;
|
||||
relatedItems.push({
|
||||
id: path,
|
||||
primary: this.hass.localize(
|
||||
primary: this._i18n.localize(
|
||||
"ui.components.navigation-picker.area_settings",
|
||||
{ area: resolvedArea.label }
|
||||
),
|
||||
@@ -550,13 +602,22 @@ export class HaNavigationPicker extends LitElement {
|
||||
return relatedItems;
|
||||
}
|
||||
|
||||
private _pathInfoOptions(): NavigationPathInfoOptions {
|
||||
return {
|
||||
localize: this._i18n.localize,
|
||||
panels: this._panels ?? {},
|
||||
areas: this._registries?.areas ?? {},
|
||||
devices: this._registries?.devices ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
if (Object.keys(this._configEntryLookup).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
const configEntries = await getConfigEntries(this._api!);
|
||||
this._configEntryLookup = Object.fromEntries(
|
||||
configEntries.map((entry) => [entry.entry_id, entry])
|
||||
);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { apiContext } from "../data/context";
|
||||
import { getAppKey } from "../data/notify_html5";
|
||||
import { showPromptDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import type { HaSwitch } from "./ha-switch";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent, type HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import "./ha-switch";
|
||||
|
||||
@@ -17,10 +21,14 @@ export const pushSupported =
|
||||
|
||||
@customElement("ha-push-notifications-toggle")
|
||||
class HaPushNotificationsToggle extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled!: boolean;
|
||||
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state() private _pushChecked: boolean =
|
||||
"Notification" in window && Notification.permission === "granted";
|
||||
|
||||
@@ -81,10 +89,10 @@ class HaPushNotificationsToggle extends LitElement {
|
||||
}
|
||||
|
||||
const name = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
title: this._localize(
|
||||
"ui.panel.profile.push_notifications.add_device_prompt.title"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
inputLabel: this._localize(
|
||||
"ui.panel.profile.push_notifications.add_device_prompt.input_label"
|
||||
),
|
||||
});
|
||||
@@ -95,7 +103,7 @@ class HaPushNotificationsToggle extends LitElement {
|
||||
|
||||
let applicationServerKey: Uint8Array<ArrayBuffer> | null;
|
||||
try {
|
||||
applicationServerKey = await getAppKey(this.hass);
|
||||
applicationServerKey = await getAppKey(this._api.callWS);
|
||||
} catch (_err) {
|
||||
applicationServerKey = null;
|
||||
}
|
||||
@@ -109,7 +117,7 @@ class HaPushNotificationsToggle extends LitElement {
|
||||
sub = await reg.pushManager.subscribe({ userVisibleOnly: true });
|
||||
}
|
||||
|
||||
await this.hass.callApi("POST", "notify.html5", {
|
||||
await this._api.callApi("POST", "notify.html5", {
|
||||
subscription: sub,
|
||||
browser: browserName,
|
||||
name,
|
||||
@@ -136,7 +144,7 @@ class HaPushNotificationsToggle extends LitElement {
|
||||
|
||||
if (!sub) return;
|
||||
|
||||
await this.hass.callApi("DELETE", "notify.html5", { subscription: sub });
|
||||
await this._api.callApi("DELETE", "notify.html5", { subscription: sub });
|
||||
await sub.unsubscribe();
|
||||
} catch (err: any) {
|
||||
const message =
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { css } from "lit";
|
||||
import { RadioListItemBase } from "@material/mwc-list/mwc-radio-list-item-base";
|
||||
import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css";
|
||||
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-radio-list-item")
|
||||
export class HaRadioListItem extends RadioListItemBase {
|
||||
async onChange(event) {
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
controlStyles,
|
||||
css`
|
||||
:host {
|
||||
--mdc-theme-secondary: var(--primary-color);
|
||||
}
|
||||
|
||||
:host([graphic="avatar"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="medium"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="large"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="control"]) .mdc-deprecated-list-item__graphic {
|
||||
margin-inline-end: var(--mdc-list-item-graphic-margin, 16px);
|
||||
margin-inline-start: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-deprecated-list-item__meta {
|
||||
flex-shrink: 0;
|
||||
direction: var(--direction);
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
.mdc-deprecated-list-item__graphic {
|
||||
margin-top: var(--radio-list-item-graphic-margin-top);
|
||||
}
|
||||
:host([graphic="icon"]) .mdc-deprecated-list-item__graphic {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: var(--mdc-list-item-graphic-margin, 32px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-radio-list-item": HaRadioListItem;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { AssistPipelineSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-assist-pipeline-picker";
|
||||
|
||||
@customElement("ha-selector-assist_pipeline")
|
||||
export class HaAssistPipelineSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: AssistPipelineSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
@@ -23,7 +20,6 @@ export class HaAssistPipelineSelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-assist-pipeline-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -6,13 +6,10 @@ import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consumeEntityStates } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { AttributeSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-attribute-picker";
|
||||
|
||||
@customElement("ha-selector-attribute")
|
||||
export class HaSelectorAttribute extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: AttributeSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
@@ -36,7 +33,6 @@ export class HaSelectorAttribute extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-entity-attribute-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${
|
||||
this.selector.attribute?.entity_id || this.context?.filter_entity
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { BackupLocationSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-mount-picker";
|
||||
|
||||
@customElement("ha-selector-backup_location")
|
||||
export class HaBackupLocationSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: BackupLocationSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
@@ -22,7 +19,6 @@ export class HaBackupLocationSelector extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`<ha-mount-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { ConversationAgentSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-conversation-agent-picker";
|
||||
|
||||
@customElement("ha-selector-conversation_agent")
|
||||
export class HaConversationAgentSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ConversationAgentSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
@@ -26,7 +23,6 @@ export class HaConversationAgentSelector extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`<ha-conversation-agent-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.language=${
|
||||
this.selector.conversation_agent?.language || this.context?.language
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import { getConfigEntries } from "../../data/config_entries";
|
||||
import { apiContext, registriesContext } from "../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
|
||||
import type { DeviceSelector } from "../../data/selector";
|
||||
@@ -20,10 +23,14 @@ import "../device/ha-devices-picker";
|
||||
|
||||
@customElement("ha-selector-device")
|
||||
export class HaDeviceSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: DeviceSelector;
|
||||
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries!: ContextType<typeof registriesContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state() private _configEntries?: ConfigEntry[];
|
||||
|
||||
private _fetchedConfigEntries = false;
|
||||
@@ -82,7 +89,7 @@ export class HaDeviceSelector extends LitElement {
|
||||
super.updated(changedProperties);
|
||||
if (!this._fetchedConfigEntries && this._hasIntegration(this.selector)) {
|
||||
this._fetchedConfigEntries = true;
|
||||
getConfigEntries(this.hass)
|
||||
getConfigEntries(this._api)
|
||||
.then((entries) => {
|
||||
this._configEntries = entries;
|
||||
})
|
||||
@@ -100,7 +107,6 @@ export class HaDeviceSelector extends LitElement {
|
||||
if (!this.selector.device?.multiple) {
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
@@ -118,7 +124,6 @@ export class HaDeviceSelector extends LitElement {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : ""}
|
||||
<ha-devices-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
@@ -137,8 +142,8 @@ export class HaDeviceSelector extends LitElement {
|
||||
}
|
||||
const deviceIntegrations = this._hasIntegration(this.selector)
|
||||
? this._deviceIntegrationLookup(
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this._registries.entities,
|
||||
this._registries.devices,
|
||||
this._configEntries
|
||||
)
|
||||
: undefined;
|
||||
@@ -154,8 +159,8 @@ export class HaDeviceSelector extends LitElement {
|
||||
filter,
|
||||
entity,
|
||||
undefined,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
this._registries.entities,
|
||||
this._registries.devices
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { EntityNameSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-name-picker";
|
||||
|
||||
@customElement("ha-selector-entity_name")
|
||||
export class HaSelectorEntityName extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
export class HaSelectorEntityName extends LitElement {
|
||||
@property({ attribute: false }) public selector!: EntityNameSelector;
|
||||
|
||||
@property() public value?: string | string[];
|
||||
@@ -28,7 +24,6 @@ export class HaSelectorEntityName extends SubscribeMixin(LitElement) {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-entity-name-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${
|
||||
this.selector.entity_name?.entity_id || this.context?.entity
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import { getConfigEntries } from "../../data/config_entries";
|
||||
import {
|
||||
apiContext,
|
||||
connectionContext,
|
||||
devicesContext,
|
||||
entitiesContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
|
||||
import type { EntitySources } from "../../data/entity/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
|
||||
@@ -15,14 +24,12 @@ import {
|
||||
computeCreateDomains,
|
||||
filterSelectorEntities,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, HomeAssistantConnection } from "../../types";
|
||||
import "../entity/ha-entities-picker";
|
||||
import "../entity/ha-entity-picker";
|
||||
|
||||
@customElement("ha-selector-entity")
|
||||
export class HaEntitySelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: EntitySelector;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
@@ -43,15 +50,31 @@ export class HaEntitySelector extends LitElement {
|
||||
|
||||
@state() private _createDomains: string[] | undefined;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
private _entities!: ContextType<typeof entitiesContext>;
|
||||
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
private _devices!: ContextType<typeof devicesContext>;
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(
|
||||
(
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
entities: HomeAssistant["entities"] | null,
|
||||
devices: HomeAssistant["devices"] | null,
|
||||
configEntries?: ConfigEntry[]
|
||||
) =>
|
||||
getDeviceIntegrationLookup(
|
||||
Object.values(entities),
|
||||
Object.values(devices),
|
||||
Object.values(entities ?? {}),
|
||||
Object.values(devices ?? {}),
|
||||
configEntries
|
||||
)
|
||||
);
|
||||
@@ -71,6 +94,8 @@ export class HaEntitySelector extends LitElement {
|
||||
|
||||
private _fetchedConfigEntries = false;
|
||||
|
||||
private _fetchedEntitySources = false;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.get("selector") && this.value !== undefined) {
|
||||
if (this.selector.entity?.multiple && !Array.isArray(this.value)) {
|
||||
@@ -101,7 +126,11 @@ export class HaEntitySelector extends LitElement {
|
||||
.includeEntities=${this.selector.entity?.include_entities}
|
||||
.excludeEntities=${this.selector.entity?.exclude_entities}
|
||||
.extraOptions=${this.selector.entity?.extra_options}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.entityFilter=${this._filterEntities(
|
||||
this.selector,
|
||||
this._entitySources,
|
||||
this._configEntries
|
||||
)}
|
||||
.createDomains=${this._createDomains}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@@ -117,7 +146,11 @@ export class HaEntitySelector extends LitElement {
|
||||
.includeEntities=${this.selector.entity.include_entities}
|
||||
.excludeEntities=${this.selector.entity.exclude_entities}
|
||||
.reorder=${this.selector.entity.reorder ?? false}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.entityFilter=${this._filterEntities(
|
||||
this.selector,
|
||||
this._entitySources,
|
||||
this._configEntries
|
||||
)}
|
||||
.createDomains=${this._createDomains}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@@ -125,36 +158,44 @@ export class HaEntitySelector extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>): void {
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
// The connection changed (e.g. reconnect); refetch config entries.
|
||||
const oldHass = changedProps.get("hass");
|
||||
if (oldHass && oldHass.connection !== this.hass.connection) {
|
||||
const oldConnection = changedProps.get("_connection") as
|
||||
HomeAssistantConnection | undefined;
|
||||
if (
|
||||
oldConnection &&
|
||||
this._connection &&
|
||||
(oldConnection.connection !== this._connection.connection ||
|
||||
(!oldConnection.connected && this._connection.connected))
|
||||
) {
|
||||
this._fetchedEntitySources = false;
|
||||
this._fetchedConfigEntries = false;
|
||||
this._configEntries = undefined;
|
||||
}
|
||||
|
||||
const needs = this._dataNeeds(this.selector);
|
||||
|
||||
if (needs.entitySources && !this._entitySources) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
if (needs.entitySources && !this._fetchedEntitySources) {
|
||||
this._fetchedEntitySources = true;
|
||||
fetchEntitySourcesWithCache(this._api.callWS, this._states ?? {})
|
||||
.then((sources) => {
|
||||
this._entitySources = sources;
|
||||
})
|
||||
.catch(() => {
|
||||
this._entitySources ??= {};
|
||||
});
|
||||
}
|
||||
|
||||
if (needs.configEntries && !this._fetchedConfigEntries) {
|
||||
this._fetchedConfigEntries = true;
|
||||
getConfigEntries(this.hass)
|
||||
getConfigEntries(this._api)
|
||||
.then((entries) => {
|
||||
this._configEntries = entries;
|
||||
})
|
||||
.catch(() => {
|
||||
// Fall back to no entries so the picker still renders. We keep
|
||||
// `_fetchedConfigEntries` set so the failed fetch is not retried on
|
||||
// every re-render; the connection-change handler above retries on
|
||||
// reconnect.
|
||||
this._configEntries = [];
|
||||
// Keep what we have, or fall back to no entries so the picker still
|
||||
// renders. A reconnect fetches again.
|
||||
this._configEntries ??= [];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -163,29 +204,36 @@ export class HaEntitySelector extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (!this.selector?.entity?.filter) {
|
||||
return true;
|
||||
}
|
||||
const deviceIntegrationLookup = this._dataNeeds(this.selector).configEntries
|
||||
? this._deviceIntegrationLookup(
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this._configEntries
|
||||
)
|
||||
: undefined;
|
||||
private _filterEntities = memoizeOne(
|
||||
(
|
||||
selector: EntitySelector,
|
||||
entitySources: EntitySources | undefined,
|
||||
configEntries: ConfigEntry[] | undefined
|
||||
) =>
|
||||
(entity: HassEntity): boolean => {
|
||||
if (!selector.entity?.filter) {
|
||||
return true;
|
||||
}
|
||||
const deviceIntegrationLookup = this._dataNeeds(selector).configEntries
|
||||
? this._deviceIntegrationLookup(
|
||||
this._entities,
|
||||
this._devices,
|
||||
configEntries
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return ensureArray(this.selector.entity.filter).some((filter) =>
|
||||
filterSelectorEntities(
|
||||
filter,
|
||||
entity,
|
||||
this._entitySources,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
deviceIntegrationLookup
|
||||
)
|
||||
);
|
||||
};
|
||||
return ensureArray(selector.entity.filter).some((filter) =>
|
||||
filterSelectorEntities(
|
||||
filter,
|
||||
entity,
|
||||
entitySources,
|
||||
this._entities ?? undefined,
|
||||
this._devices ?? undefined,
|
||||
deviceIntegrationLookup
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import { getConfigEntries } from "../../data/config_entries";
|
||||
import { apiContext, registriesContext } from "../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
|
||||
import type { FloorSelector } from "../../data/selector";
|
||||
@@ -20,7 +23,11 @@ import "../ha-floors-picker";
|
||||
|
||||
@customElement("ha-selector-floor")
|
||||
export class HaFloorSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries!: ContextType<typeof registriesContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@property({ attribute: false }) public selector!: FloorSelector;
|
||||
|
||||
@@ -77,7 +84,7 @@ export class HaFloorSelector extends LitElement {
|
||||
protected updated(): void {
|
||||
if (!this._fetchedConfigEntries && this._hasIntegration(this.selector)) {
|
||||
this._fetchedConfigEntries = true;
|
||||
getConfigEntries(this.hass)
|
||||
getConfigEntries(this._api)
|
||||
.then((entries) => {
|
||||
this._configEntries = entries;
|
||||
})
|
||||
@@ -95,7 +102,6 @@ export class HaFloorSelector extends LitElement {
|
||||
if (!this.selector.floor?.multiple) {
|
||||
return html`
|
||||
<ha-floor-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
@@ -114,7 +120,6 @@ export class HaFloorSelector extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-floors-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.pickFloorLabel=${this.label}
|
||||
@@ -141,8 +146,8 @@ export class HaFloorSelector extends LitElement {
|
||||
filter,
|
||||
entity,
|
||||
undefined,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
this._registries.entities,
|
||||
this._registries.devices
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -154,8 +159,8 @@ export class HaFloorSelector extends LitElement {
|
||||
|
||||
const deviceIntegrations = this._hasIntegration(this.selector)
|
||||
? this._deviceIntegrationLookup(
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this._registries.entities,
|
||||
this._registries.devices,
|
||||
this._configEntries
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -3,13 +3,10 @@ import { customElement, property } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LabelSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-labels-picker";
|
||||
|
||||
@customElement("ha-selector-label")
|
||||
export class HaLabelSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public value?: string | string[];
|
||||
|
||||
@property() public name?: string;
|
||||
@@ -31,7 +28,6 @@ export class HaLabelSelector extends LitElement {
|
||||
return html`
|
||||
<ha-labels-picker
|
||||
no-add
|
||||
.hass=${this.hass}
|
||||
.value=${ensureArray(this.value ?? [])}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@@ -45,7 +41,6 @@ export class HaLabelSelector extends LitElement {
|
||||
return html`
|
||||
<ha-label-picker
|
||||
no-add
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -186,7 +186,7 @@ export class HaLocationSelector extends LitElement {
|
||||
static styles = css`
|
||||
ha-locations-editor {
|
||||
display: block;
|
||||
height: 400px;
|
||||
--ha-locations-editor-map-height: 400px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
p {
|
||||
|
||||
@@ -2,14 +2,11 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { NavigationSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ActionRelatedContext } from "../../panels/lovelace/components/hui-action-editor";
|
||||
import "../ha-navigation-picker";
|
||||
|
||||
@customElement("ha-selector-navigation")
|
||||
export class HaNavigationSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: NavigationSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -27,7 +24,6 @@ export class HaNavigationSelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-navigation-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
|
||||
@@ -5,15 +5,31 @@ import {
|
||||
mdiPencil,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import type { ObjectSelector } from "../../data/selector";
|
||||
import { formatSelectorValue } from "../../data/selector/format_selector_value";
|
||||
import {
|
||||
formatSelectorValue,
|
||||
type FormatSelectorValueOptions,
|
||||
} from "../../data/selector/format_selector_value";
|
||||
import { showFormDialog } from "../../dialogs/form/show-form-dialog";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type {
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
HomeAssistantRegistries,
|
||||
} from "../../types";
|
||||
import { computeInitialHaFormData } from "../ha-form/compute-initial-ha-form-data";
|
||||
import type { HaFormSchema } from "../ha-form/types";
|
||||
import "../ha-input-helper-text";
|
||||
@@ -26,8 +42,6 @@ import { deepEqual } from "../../common/util/deep-equal";
|
||||
|
||||
@customElement("ha-selector-object")
|
||||
export class HaObjectSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ObjectSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
@@ -47,6 +61,28 @@ export class HaObjectSelector extends LitElement {
|
||||
values?: Record<string, string>
|
||||
) => string;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@state()
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries!: HomeAssistantRegistries;
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: HassEntities;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
@transform<
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantFormatters["formatEntityName"]
|
||||
>({
|
||||
transformer: (formatters) => formatters?.formatEntityName,
|
||||
})
|
||||
private _formatEntityName!: HomeAssistantFormatters["formatEntityName"];
|
||||
|
||||
@query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
private _valueChangedFromChild = false;
|
||||
@@ -83,7 +119,12 @@ export class HaObjectSelector extends LitElement {
|
||||
private _formatField(item: any, field: string): string {
|
||||
const fields = this.selector.object!.fields!;
|
||||
const value = formatSelectorValue(
|
||||
this.hass,
|
||||
this._formatOptions(
|
||||
this._i18n,
|
||||
this._registries,
|
||||
this._states,
|
||||
this._formatEntityName
|
||||
),
|
||||
item[field],
|
||||
fields[field]?.selector
|
||||
);
|
||||
@@ -148,14 +189,14 @@ export class HaObjectSelector extends LitElement {
|
||||
slot="end"
|
||||
.item=${item}
|
||||
.index=${index}
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
.label=${this._i18n.localize("ui.common.edit")}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._editItem}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.index=${index}
|
||||
.label=${this.hass.localize("ui.common.delete")}
|
||||
.label=${this._i18n.localize("ui.common.delete")}
|
||||
.path=${multiple ? mdiDelete : mdiClose}
|
||||
@click=${this._deleteItem}
|
||||
></ha-icon-button>
|
||||
@@ -180,7 +221,7 @@ export class HaObjectSelector extends LitElement {
|
||||
</ha-list-base>
|
||||
</ha-sortable>
|
||||
<ha-button appearance="filled" @click=${this._addItem}>
|
||||
${this.hass.localize("ui.common.add")}
|
||||
${this._i18n.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
@@ -196,7 +237,7 @@ export class HaObjectSelector extends LitElement {
|
||||
</ha-list-base>`
|
||||
: html`
|
||||
<ha-button appearance="filled" @click=${this._addItem}>
|
||||
${this.hass.localize("ui.common.add")}
|
||||
${this._i18n.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
`
|
||||
}
|
||||
@@ -219,6 +260,23 @@ export class HaObjectSelector extends LitElement {
|
||||
} `;
|
||||
}
|
||||
|
||||
private _formatOptions = memoizeOne(
|
||||
(
|
||||
i18n: HomeAssistantInternationalization,
|
||||
registries: HomeAssistantRegistries,
|
||||
states: HassEntities,
|
||||
formatEntityName: HomeAssistantFormatters["formatEntityName"]
|
||||
): FormatSelectorValueOptions => ({
|
||||
locale: i18n.locale,
|
||||
localize: i18n.localize,
|
||||
states,
|
||||
devices: registries.devices,
|
||||
areas: registries.areas,
|
||||
floors: registries.floors,
|
||||
formatEntityName,
|
||||
})
|
||||
);
|
||||
|
||||
private _schema = memoizeOne((selector: ObjectSelector) => {
|
||||
if (!selector.object || !selector.object.fields) {
|
||||
return [];
|
||||
@@ -262,12 +320,12 @@ export class HaObjectSelector extends LitElement {
|
||||
};
|
||||
|
||||
const newItem = await showFormDialog(this, {
|
||||
title: this.hass.localize("ui.common.add"),
|
||||
title: this._i18n.localize("ui.common.add"),
|
||||
schema,
|
||||
data,
|
||||
computeLabel: this._computeLabel,
|
||||
computeHelper: this._computeHelper,
|
||||
submitText: this.hass.localize("ui.common.add"),
|
||||
submitText: this._i18n.localize("ui.common.add"),
|
||||
});
|
||||
|
||||
if (newItem === null) {
|
||||
@@ -290,12 +348,12 @@ export class HaObjectSelector extends LitElement {
|
||||
const index = ev.currentTarget.index;
|
||||
|
||||
const updatedItem = await showFormDialog(this, {
|
||||
title: this.hass.localize("ui.common.edit"),
|
||||
title: this._i18n.localize("ui.common.edit"),
|
||||
schema: this._schema(this.selector),
|
||||
data: item,
|
||||
computeLabel: this._computeLabel,
|
||||
computeHelper: this._computeHelper,
|
||||
submitText: this.hass.localize("ui.common.save"),
|
||||
submitText: this._i18n.localize("ui.common.save"),
|
||||
});
|
||||
|
||||
if (updatedItem === null) {
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { mdiDragHorizontalVariant } from "@mdi/js";
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../../types";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-checkbox";
|
||||
import "../ha-dropdown-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-button";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-select";
|
||||
import "../ha-select-box";
|
||||
import "../ha-sortable";
|
||||
import "../ha-svg-icon";
|
||||
import "../radio/ha-radio-group";
|
||||
import "../radio/ha-radio-option";
|
||||
|
||||
@@ -28,10 +30,7 @@ import "../radio/ha-radio-option";
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale!: FrontendLocaleData;
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@property({ attribute: false }) public selector!: SelectSelector;
|
||||
|
||||
@@ -48,6 +47,8 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
private _itemMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
@@ -83,7 +84,11 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
if (this.selector.select?.sort) {
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, this._locale.language)
|
||||
caseInsensitiveStringCompare(
|
||||
a.label,
|
||||
b.label,
|
||||
this._i18n.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,8 +141,8 @@ export class HaSelectSelector extends LitElement {
|
||||
const value =
|
||||
!this.value || this.value === "" ? [] : ensureArray(this.value);
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
<div class="checkbox-list">
|
||||
${this.label} ${this._renderHelper()}
|
||||
${options.map(
|
||||
(item: SelectOption) => html`
|
||||
<ha-checkbox
|
||||
@@ -151,7 +156,6 @@ export class HaSelectSelector extends LitElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -159,67 +163,80 @@ export class HaSelectSelector extends LitElement {
|
||||
const value =
|
||||
!this.value || this.value === "" ? [] : ensureArray(this.value);
|
||||
|
||||
const addLabel = this._i18n.localize("ui.common.add");
|
||||
|
||||
return html`
|
||||
${
|
||||
value?.length
|
||||
? html`
|
||||
<ha-sortable
|
||||
no-style
|
||||
.disabled=${!this.selector.select.reorder}
|
||||
@item-moved=${this._itemMoved}
|
||||
handle-selector="button.primary.action"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
value,
|
||||
(item) => item,
|
||||
(item, idx) => {
|
||||
const label =
|
||||
options.find((option) => option.value === item)
|
||||
?.label || item;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
.label=${label}
|
||||
.title=${label}
|
||||
selected
|
||||
>
|
||||
${
|
||||
this.selector.select?.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
options.find((option) => option.value === item)
|
||||
?.label || item
|
||||
}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
`
|
||||
this.label
|
||||
? html`<span id="label" class="label chips-label"
|
||||
>${this.label}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<ha-generic-picker
|
||||
no-sort
|
||||
.helper=${this.helper}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${""}
|
||||
.addButtonLabel=${this.label}
|
||||
.getItems=${this._getItems(options, value, true)}
|
||||
.allowCustomValue=${this.selector.select.custom_value ?? false}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
></ha-generic-picker>
|
||||
>
|
||||
<ha-sortable
|
||||
slot="field"
|
||||
no-style
|
||||
.disabled=${!this.selector.select.reorder}
|
||||
@item-moved=${this._itemMoved}
|
||||
draggable-selector="ha-input-chip"
|
||||
handle-selector="button.primary.action"
|
||||
>
|
||||
<ha-chip-set
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
${repeat(
|
||||
value,
|
||||
(item) => item,
|
||||
(item, idx) => {
|
||||
const label =
|
||||
options.find((option) => option.value === item)?.label ||
|
||||
item;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
.label=${label}
|
||||
.title=${label}
|
||||
selected
|
||||
>
|
||||
${
|
||||
this.selector.select?.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
<ha-button
|
||||
size="s"
|
||||
appearance="filled"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._openPicker}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${addLabel}
|
||||
</ha-button>
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</ha-generic-picker>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -264,6 +281,11 @@ export class HaSelectSelector extends LitElement {
|
||||
: "";
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._picker?.open();
|
||||
}
|
||||
|
||||
private _getOptions = memoizeOne(
|
||||
(selector: SelectSelector) =>
|
||||
selector.select?.options?.map((option) =>
|
||||
@@ -404,19 +426,37 @@ export class HaSelectSelector extends LitElement {
|
||||
ha-dropdown-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-chip-set {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.chips-label {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
|
||||
ha-sortable ha-chip-set {
|
||||
background-color: var(--mdc-text-field-fill-color);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-normal);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-select-box + ha-input-helper-text {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
ha-generic-picker + ha-input-helper-text {
|
||||
padding: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.checkbox-list ha-input-helper-text {
|
||||
padding: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiClose, mdiConnection, mdiMemory, mdiPencil, mdiUsb } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
@@ -6,9 +7,17 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
apiContext,
|
||||
areasContext,
|
||||
configContext,
|
||||
devicesContext,
|
||||
internationalizationContext,
|
||||
} from "../../data/context";
|
||||
import type { SerialPortSelector } from "../../data/selector";
|
||||
import { listSerialPorts, type SerialPort } from "../../data/usb";
|
||||
import { mdiEsphomeLogo } from "../../resources/esphome-logo-svg";
|
||||
@@ -110,8 +119,6 @@ const getPortType = (
|
||||
|
||||
@customElement("ha-selector-serial_port")
|
||||
export class HaSerialPortSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: SerialPortSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -132,6 +139,25 @@ export class HaSerialPortSelector extends LitElement {
|
||||
|
||||
@state() private _manualEntry = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
private _devices?: ContextType<typeof devicesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
private _areas?: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@query("ha-input") private _input?: HTMLElement;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
@@ -155,9 +181,8 @@ export class HaSerialPortSelector extends LitElement {
|
||||
|
||||
private _canLoadPorts(): boolean {
|
||||
return Boolean(
|
||||
this.hass &&
|
||||
this.hass.user?.is_admin &&
|
||||
isComponentLoaded(this.hass.config, "usb")
|
||||
this._config?.user?.is_admin &&
|
||||
isComponentLoaded(this._config.config, "usb")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,7 +199,7 @@ export class HaSerialPortSelector extends LitElement {
|
||||
}
|
||||
this._loadInFlight = true;
|
||||
try {
|
||||
this._serialPorts = await listSerialPorts(this.hass);
|
||||
this._serialPorts = await listSerialPorts(this._api.callWS);
|
||||
} catch (err: unknown) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
@@ -213,8 +238,8 @@ export class HaSerialPortSelector extends LitElement {
|
||||
(
|
||||
ports: SerialPort[],
|
||||
language: string,
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
devices: HomeAssistant["devices"] | undefined,
|
||||
areas: HomeAssistant["areas"] | undefined,
|
||||
localize: HomeAssistant["localize"],
|
||||
recommendedDomains: Set<string>
|
||||
): Record<SerialPortType, SerialPickerItem[]> => {
|
||||
@@ -246,13 +271,13 @@ export class HaSerialPortSelector extends LitElement {
|
||||
const url = new URL(port.device);
|
||||
primary = url.searchParams.get("port_name") || port.device;
|
||||
const configEntryId = url.pathname.replace(/^\/+/, "");
|
||||
const device = Object.values(devices).find(
|
||||
const device = Object.values(devices ?? {}).find(
|
||||
(d) => d.primary_config_entry === configEntryId
|
||||
);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName =
|
||||
device && device.area_id
|
||||
? areas[device.area_id]?.name
|
||||
? areas?.[device.area_id]?.name
|
||||
: undefined;
|
||||
if (deviceName && areaName) {
|
||||
secondary = localize(
|
||||
@@ -343,11 +368,11 @@ export class HaSerialPortSelector extends LitElement {
|
||||
private _sectionLabel(type: SerialPortType): string {
|
||||
const key = `ui.components.selectors.serial_port.type.${type}` as const;
|
||||
if (type === "recommended" && this._selectorDomain) {
|
||||
return this.hass.localize(key, {
|
||||
integration: integrationName(this.hass.localize, this._selectorDomain),
|
||||
return this._i18n.localize(key, {
|
||||
integration: integrationName(this._i18n.localize, this._selectorDomain),
|
||||
});
|
||||
}
|
||||
return this.hass.localize(key);
|
||||
return this._i18n.localize(key);
|
||||
}
|
||||
|
||||
private get _selectorDomain(): string | undefined {
|
||||
@@ -388,10 +413,10 @@ export class HaSerialPortSelector extends LitElement {
|
||||
|
||||
const grouped = this._buildGroupedItems(
|
||||
this._serialPorts,
|
||||
this.hass.locale.language,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.localize,
|
||||
this._i18n.locale.language,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._i18n.localize,
|
||||
this._recommendedDomains
|
||||
);
|
||||
|
||||
@@ -425,7 +450,7 @@ export class HaSerialPortSelector extends LitElement {
|
||||
private _getAdditionalItems = (): PickerComboBoxItem[] => [
|
||||
{
|
||||
id: MANUAL_ENTRY_ID,
|
||||
primary: this.hass.localize(
|
||||
primary: this._i18n.localize(
|
||||
"ui.components.selectors.serial_port.enter_manually"
|
||||
),
|
||||
icon_path: mdiPencil,
|
||||
@@ -482,7 +507,8 @@ export class HaSerialPortSelector extends LitElement {
|
||||
};
|
||||
|
||||
protected render() {
|
||||
const usbLoaded = this.hass && isComponentLoaded(this.hass.config, "usb");
|
||||
const usbLoaded =
|
||||
!!this._config && isComponentLoaded(this._config.config, "usb");
|
||||
|
||||
if (!usbLoaded || !this._serialPorts || this._manualEntry) {
|
||||
return html`
|
||||
@@ -539,10 +565,10 @@ export class HaSerialPortSelector extends LitElement {
|
||||
const item = Object.values(
|
||||
this._buildGroupedItems(
|
||||
this._serialPorts,
|
||||
this.hass.locale.language,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.localize,
|
||||
this._i18n.locale.language,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._i18n.localize,
|
||||
this._recommendedDomains
|
||||
)
|
||||
)
|
||||
@@ -562,10 +588,10 @@ export class HaSerialPortSelector extends LitElement {
|
||||
}
|
||||
const grouped = this._buildGroupedItems(
|
||||
this._serialPorts,
|
||||
this.hass.locale.language,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.localize,
|
||||
this._i18n.locale.language,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._i18n.localize,
|
||||
this._recommendedDomains
|
||||
);
|
||||
return SECTION_ORDER.filter((type) => grouped[type].length).map((type) => ({
|
||||
|
||||
Loaded 100 of 509 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user