mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-17 15:00:31 +00:00
Compare commits
19 Commits
renovate/n
...
add-automa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f3a2b8418 | ||
|
|
12c1e4eec4 | ||
|
|
b704b621f2 | ||
|
|
0eb993a8df | ||
|
|
cda97766ac | ||
|
|
effba9b918 | ||
|
|
c848673b1f | ||
|
|
074095d3dc | ||
|
|
1bd1e015ff | ||
|
|
7588490419 | ||
|
|
2e80a3ddab | ||
|
|
e6936a9294 | ||
|
|
5ad73287a2 | ||
|
|
591b464508 | ||
|
|
acf963d38c | ||
|
|
68f383c293 | ||
|
|
0a25d8106c | ||
|
|
5c3cf17df9 | ||
|
|
e905fa6f23 |
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -36,14 +36,14 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -57,4 +57,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
"home-assistant-js-websocket": "9.5.0",
|
"home-assistant-js-websocket": "9.5.0",
|
||||||
"idb-keyval": "6.2.2",
|
"idb-keyval": "6.2.2",
|
||||||
"intl-messageformat": "10.7.18",
|
"intl-messageformat": "10.7.18",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.1",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||||
"leaflet.markercluster": "1.5.3",
|
"leaflet.markercluster": "1.5.3",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class HaFilterChip extends FilterChip {
|
|||||||
var(--rgb-primary-text-color),
|
var(--rgb-primary-text-color),
|
||||||
0.15
|
0.15
|
||||||
);
|
);
|
||||||
|
--_label-text-font: var(--ha-font-family-body);
|
||||||
border-radius: var(--ha-border-radius-md);
|
border-radius: var(--ha-border-radius-md);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -154,7 +154,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this._getLabelsMemoized(
|
return this._getLabelsMemoized(
|
||||||
this.hass,
|
this.hass.states,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.entities,
|
||||||
this._labels,
|
this._labels,
|
||||||
this.includeDomains,
|
this.includeDomains,
|
||||||
this.excludeDomains,
|
this.excludeDomains,
|
||||||
|
|||||||
28
src/components/ha-section-title.ts
Normal file
28
src/components/ha-section-title.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
|
||||||
|
@customElement("ha-section-title")
|
||||||
|
class HaSectionTitle extends LitElement {
|
||||||
|
protected render() {
|
||||||
|
return html`<slot></slot>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||||
|
padding: var(--ha-space-1) var(--ha-space-2);
|
||||||
|
font-weight: var(--ha-font-weight-bold);
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
min-height: var(--ha-space-6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-section-title": HaSectionTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -858,7 +858,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
if (!filterType || filterType === "label") {
|
if (!filterType || filterType === "label") {
|
||||||
let labels = this._getLabelsMemoized(
|
let labels = this._getLabelsMemoized(
|
||||||
this.hass,
|
this.hass.states,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.entities,
|
||||||
this._labelRegistry,
|
this._labelRegistry,
|
||||||
includeDomains,
|
includeDomains,
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
@@ -24,11 +24,54 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
|
|||||||
area?: AreaRegistryEntry;
|
area?: AreaRegistryEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
|
||||||
|
floor?: FloorRegistryEntry;
|
||||||
|
areas: FloorComboBoxItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
|
||||||
|
areas: FloorComboBoxItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AreaFloorValue {
|
export interface AreaFloorValue {
|
||||||
id: string;
|
id: string;
|
||||||
type: "floor" | "area";
|
type: "floor" | "area";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAreasNestedInFloors = (
|
||||||
|
states: HomeAssistant["states"],
|
||||||
|
haFloors: HomeAssistant["floors"],
|
||||||
|
haAreas: HomeAssistant["areas"],
|
||||||
|
haDevices: HomeAssistant["devices"],
|
||||||
|
haEntities: HomeAssistant["entities"],
|
||||||
|
formatId: (value: AreaFloorValue) => string,
|
||||||
|
includeDomains?: string[],
|
||||||
|
excludeDomains?: string[],
|
||||||
|
includeDeviceClasses?: string[],
|
||||||
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
|
excludeAreas?: string[],
|
||||||
|
excludeFloors?: string[],
|
||||||
|
includeEmptyFloors = false
|
||||||
|
) =>
|
||||||
|
getAreasAndFloorsItems(
|
||||||
|
states,
|
||||||
|
haFloors,
|
||||||
|
haAreas,
|
||||||
|
haDevices,
|
||||||
|
haEntities,
|
||||||
|
formatId,
|
||||||
|
includeDomains,
|
||||||
|
excludeDomains,
|
||||||
|
includeDeviceClasses,
|
||||||
|
deviceFilter,
|
||||||
|
entityFilter,
|
||||||
|
excludeAreas,
|
||||||
|
excludeFloors,
|
||||||
|
includeEmptyFloors,
|
||||||
|
true
|
||||||
|
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
|
||||||
|
|
||||||
export const getAreasAndFloors = (
|
export const getAreasAndFloors = (
|
||||||
states: HomeAssistant["states"],
|
states: HomeAssistant["states"],
|
||||||
haFloors: HomeAssistant["floors"],
|
haFloors: HomeAssistant["floors"],
|
||||||
@@ -42,8 +85,47 @@ export const getAreasAndFloors = (
|
|||||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
excludeAreas?: string[],
|
excludeAreas?: string[],
|
||||||
excludeFloors?: string[]
|
excludeFloors?: string[],
|
||||||
): FloorComboBoxItem[] => {
|
includeEmptyFloors = false
|
||||||
|
) =>
|
||||||
|
getAreasAndFloorsItems(
|
||||||
|
states,
|
||||||
|
haFloors,
|
||||||
|
haAreas,
|
||||||
|
haDevices,
|
||||||
|
haEntities,
|
||||||
|
formatId,
|
||||||
|
includeDomains,
|
||||||
|
excludeDomains,
|
||||||
|
includeDeviceClasses,
|
||||||
|
deviceFilter,
|
||||||
|
entityFilter,
|
||||||
|
excludeAreas,
|
||||||
|
excludeFloors,
|
||||||
|
includeEmptyFloors
|
||||||
|
) as FloorComboBoxItem[];
|
||||||
|
|
||||||
|
const getAreasAndFloorsItems = (
|
||||||
|
states: HomeAssistant["states"],
|
||||||
|
haFloors: HomeAssistant["floors"],
|
||||||
|
haAreas: HomeAssistant["areas"],
|
||||||
|
haDevices: HomeAssistant["devices"],
|
||||||
|
haEntities: HomeAssistant["entities"],
|
||||||
|
formatId: (value: AreaFloorValue) => string,
|
||||||
|
includeDomains?: string[],
|
||||||
|
excludeDomains?: string[],
|
||||||
|
includeDeviceClasses?: string[],
|
||||||
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
|
excludeAreas?: string[],
|
||||||
|
excludeFloors?: string[],
|
||||||
|
includeEmptyFloors = false,
|
||||||
|
nested = false
|
||||||
|
): (
|
||||||
|
| FloorComboBoxItem
|
||||||
|
| FloorNestedComboBoxItem
|
||||||
|
| UnassignedAreasFloorComboBoxItem
|
||||||
|
)[] => {
|
||||||
const floors = Object.values(haFloors);
|
const floors = Object.values(haFloors);
|
||||||
const areas = Object.values(haAreas);
|
const areas = Object.values(haAreas);
|
||||||
const devices = Object.values(haDevices);
|
const devices = Object.values(haDevices);
|
||||||
@@ -189,6 +271,14 @@ export const getAreasAndFloors = (
|
|||||||
|
|
||||||
const compare = floorCompare(haFloors);
|
const compare = floorCompare(haFloors);
|
||||||
|
|
||||||
|
if (includeEmptyFloors) {
|
||||||
|
Object.values(haFloors).forEach((floor) => {
|
||||||
|
if (!floorAreaLookup[floor.floor_id]) {
|
||||||
|
floorAreaLookup[floor.floor_id] = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const floorAreaEntries: [
|
const floorAreaEntries: [
|
||||||
FloorRegistryEntry | undefined,
|
FloorRegistryEntry | undefined,
|
||||||
@@ -200,9 +290,15 @@ export const getAreasAndFloors = (
|
|||||||
})
|
})
|
||||||
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
|
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
|
||||||
|
|
||||||
const items: FloorComboBoxItem[] = [];
|
const items: (
|
||||||
|
| FloorComboBoxItem
|
||||||
|
| FloorNestedComboBoxItem
|
||||||
|
| UnassignedAreasFloorComboBoxItem
|
||||||
|
)[] = [];
|
||||||
|
|
||||||
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
||||||
|
let floorItem: FloorComboBoxItem | FloorNestedComboBoxItem;
|
||||||
|
|
||||||
if (floor) {
|
if (floor) {
|
||||||
const floorName = computeFloorName(floor);
|
const floorName = computeFloorName(floor);
|
||||||
|
|
||||||
@@ -213,7 +309,7 @@ export const getAreasAndFloors = (
|
|||||||
})
|
})
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
items.push({
|
floorItem = {
|
||||||
id: formatId({ id: floor.floor_id, type: "floor" }),
|
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||||
type: "floor",
|
type: "floor",
|
||||||
primary: floorName,
|
primary: floorName,
|
||||||
@@ -225,10 +321,9 @@ export const getAreasAndFloors = (
|
|||||||
...floor.aliases,
|
...floor.aliases,
|
||||||
...areaSearchLabels,
|
...areaSearchLabels,
|
||||||
],
|
],
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
items.push(
|
const floorAreasItems = floorAreas.map((area) => {
|
||||||
...floorAreas.map((area) => {
|
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
const areaName = computeAreaName(area) || area.area_id;
|
||||||
return {
|
return {
|
||||||
id: formatId({ id: area.area_id, type: "area" }),
|
id: formatId({ id: area.area_id, type: "area" }),
|
||||||
@@ -238,12 +333,20 @@ export const getAreasAndFloors = (
|
|||||||
icon: area.icon || undefined,
|
icon: area.icon || undefined,
|
||||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||||
};
|
};
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
items.push(
|
if (floor) {
|
||||||
...unassignedAreas.map((area) => {
|
items.push(floorItem!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nested && floor) {
|
||||||
|
(floorItem! as FloorNestedComboBoxItem).areas = floorAreasItems;
|
||||||
|
} else {
|
||||||
|
items.push(...floorAreasItems);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unassignedAreaItems = unassignedAreas.map((area) => {
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
const areaName = computeAreaName(area) || area.area_id;
|
||||||
return {
|
return {
|
||||||
id: formatId({ id: area.area_id, type: "area" }),
|
id: formatId({ id: area.area_id, type: "area" }),
|
||||||
@@ -253,8 +356,15 @@ export const getAreasAndFloors = (
|
|||||||
icon: area.icon || undefined,
|
icon: area.icon || undefined,
|
||||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
if (nested && unassignedAreaItems.length) {
|
||||||
|
items.push({
|
||||||
|
areas: unassignedAreaItems,
|
||||||
|
} as UnassignedAreasFloorComboBoxItem);
|
||||||
|
} else {
|
||||||
|
items.push(...unassignedAreaItems);
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { stringCompare } from "../common/string/compare";
|
import { stringCompare } from "../common/string/compare";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import type { DeviceRegistryEntry } from "./device_registry";
|
import type { DeviceRegistryEntry } from "./device_registry";
|
||||||
import type { EntityRegistryEntry } from "./entity_registry";
|
import type {
|
||||||
|
EntityRegistryDisplayEntry,
|
||||||
|
EntityRegistryEntry,
|
||||||
|
} from "./entity_registry";
|
||||||
import type { RegistryEntry } from "./registry";
|
import type { RegistryEntry } from "./registry";
|
||||||
|
|
||||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||||
@@ -18,7 +21,10 @@ export interface AreaRegistryEntry extends RegistryEntry {
|
|||||||
temperature_entity_id: string | null;
|
temperature_entity_id: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
|
export type AreaEntityLookup = Record<
|
||||||
|
string,
|
||||||
|
(EntityRegistryEntry | EntityRegistryDisplayEntry)[]
|
||||||
|
>;
|
||||||
|
|
||||||
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
|
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
|
||||||
|
|
||||||
@@ -60,11 +66,17 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getAreaEntityLookup = (
|
export const getAreaEntityLookup = (
|
||||||
entities: EntityRegistryEntry[]
|
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
|
||||||
|
filterHidden = false
|
||||||
): AreaEntityLookup => {
|
): AreaEntityLookup => {
|
||||||
const areaEntityLookup: AreaEntityLookup = {};
|
const areaEntityLookup: AreaEntityLookup = {};
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
if (!entity.area_id) {
|
if (
|
||||||
|
!entity.area_id ||
|
||||||
|
(filterHidden &&
|
||||||
|
((entity as EntityRegistryDisplayEntry).hidden ||
|
||||||
|
(entity as EntityRegistryEntry).hidden_by))
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!(entity.area_id in areaEntityLookup)) {
|
if (!(entity.area_id in areaEntityLookup)) {
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ export type DeviceEntityDisplayLookup = Record<
|
|||||||
EntityRegistryDisplayEntry[]
|
EntityRegistryDisplayEntry[]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type DeviceEntityLookup = Record<string, EntityRegistryEntry[]>;
|
export type DeviceEntityLookup = Record<
|
||||||
|
string,
|
||||||
|
(EntityRegistryEntry | EntityRegistryDisplayEntry)[]
|
||||||
|
>;
|
||||||
|
|
||||||
export interface DeviceRegistryEntryMutableParams {
|
export interface DeviceRegistryEntryMutableParams {
|
||||||
area_id?: string | null;
|
area_id?: string | null;
|
||||||
@@ -107,11 +110,17 @@ export const sortDeviceRegistryByName = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const getDeviceEntityLookup = (
|
export const getDeviceEntityLookup = (
|
||||||
entities: EntityRegistryEntry[]
|
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
|
||||||
|
filterHidden = false
|
||||||
): DeviceEntityLookup => {
|
): DeviceEntityLookup => {
|
||||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
if (!entity.device_id) {
|
if (
|
||||||
|
!entity.device_id ||
|
||||||
|
(filterHidden &&
|
||||||
|
((entity as EntityRegistryDisplayEntry).hidden ||
|
||||||
|
(entity as EntityRegistryEntry).hidden_by))
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!(entity.device_id in deviceEntityLookup)) {
|
if (!(entity.device_id in deviceEntityLookup)) {
|
||||||
|
|||||||
@@ -101,7 +101,10 @@ export const deleteLabelRegistryEntry = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getLabels = (
|
export const getLabels = (
|
||||||
hass: HomeAssistant,
|
hassStates: HomeAssistant["states"],
|
||||||
|
hassAreas: HomeAssistant["areas"],
|
||||||
|
hassDevices: HomeAssistant["devices"],
|
||||||
|
hassEntities: HomeAssistant["entities"],
|
||||||
labels?: LabelRegistryEntry[],
|
labels?: LabelRegistryEntry[],
|
||||||
includeDomains?: string[],
|
includeDomains?: string[],
|
||||||
excludeDomains?: string[],
|
excludeDomains?: string[],
|
||||||
@@ -115,8 +118,8 @@ export const getLabels = (
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const devices = Object.values(hass.devices);
|
const devices = Object.values(hassDevices);
|
||||||
const entities = Object.values(hass.entities);
|
const entities = Object.values(hassEntities);
|
||||||
|
|
||||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||||
@@ -170,7 +173,7 @@ export const getLabels = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return deviceEntityLookup[device.id].some((entity) => {
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
const stateObj = hass.states[entity.entity_id];
|
const stateObj = hassStates[entity.entity_id];
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -181,7 +184,7 @@ export const getLabels = (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
inputEntities = inputEntities!.filter((entity) => {
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
const stateObj = hass.states[entity.entity_id];
|
const stateObj = hassStates[entity.entity_id];
|
||||||
return (
|
return (
|
||||||
stateObj.attributes.device_class &&
|
stateObj.attributes.device_class &&
|
||||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||||
@@ -200,7 +203,7 @@ export const getLabels = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return deviceEntityLookup[device.id].some((entity) => {
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
const stateObj = hass.states[entity.entity_id];
|
const stateObj = hassStates[entity.entity_id];
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -208,7 +211,7 @@ export const getLabels = (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
inputEntities = inputEntities!.filter((entity) => {
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
const stateObj = hass.states[entity.entity_id];
|
const stateObj = hassStates[entity.entity_id];
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -245,7 +248,7 @@ export const getLabels = (
|
|||||||
|
|
||||||
if (areaIds) {
|
if (areaIds) {
|
||||||
areaIds.forEach((areaId) => {
|
areaIds.forEach((areaId) => {
|
||||||
const area = hass.areas[areaId];
|
const area = hassAreas[areaId];
|
||||||
area.labels.forEach((label) => usedLabels.add(label));
|
area.labels.forEach((label) => usedLabels.add(label));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import type { CSSResultGroup } from "lit";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { createCloseHeading } from "../../components/ha-dialog";
|
import "../../components/ha-wa-dialog";
|
||||||
|
import "../../components/ha-dialog-footer";
|
||||||
import "../../components/ha-formfield";
|
import "../../components/ha-formfield";
|
||||||
import "../../components/ha-switch";
|
import "../../components/ha-switch";
|
||||||
import "../../components/ha-button";
|
import "../../components/ha-button";
|
||||||
@@ -28,6 +29,8 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
|
|
||||||
@state() private _submitting = false;
|
@state() private _submitting = false;
|
||||||
|
|
||||||
|
@state() private _open = false;
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
params: ConfigEntrySystemOptionsDialogParams
|
params: ConfigEntrySystemOptionsDialogParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -35,9 +38,14 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._disableNewEntities = params.entry.pref_disable_new_entities;
|
this._disableNewEntities = params.entry.pref_disable_new_entities;
|
||||||
this._disablePolling = params.entry.pref_disable_polling;
|
this._disablePolling = params.entry.pref_disable_polling;
|
||||||
|
this._open = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
|
this._open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dialogClosed(): void {
|
||||||
this._error = "";
|
this._error = "";
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
@@ -49,18 +57,19 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-wa-dialog
|
||||||
open
|
.hass=${this.hass}
|
||||||
@closed=${this.closeDialog}
|
.open=${this._open}
|
||||||
.heading=${createCloseHeading(
|
header-title=${this.hass.localize(
|
||||||
this.hass,
|
"ui.dialogs.config_entry_system_options.title",
|
||||||
this.hass.localize("ui.dialogs.config_entry_system_options.title", {
|
{
|
||||||
integration:
|
integration:
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
`component.${this._params.entry.domain}.title`
|
`component.${this._params.entry.domain}.title`
|
||||||
) || this._params.entry.domain,
|
) || this._params.entry.domain,
|
||||||
})
|
}
|
||||||
)}
|
)}
|
||||||
|
@closed=${this._dialogClosed}
|
||||||
>
|
>
|
||||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||||
<ha-formfield
|
<ha-formfield
|
||||||
@@ -82,10 +91,10 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
</p>`}
|
</p>`}
|
||||||
>
|
>
|
||||||
<ha-switch
|
<ha-switch
|
||||||
|
autofocus
|
||||||
.checked=${!this._disableNewEntities}
|
.checked=${!this._disableNewEntities}
|
||||||
@change=${this._disableNewEntitiesChanged}
|
@change=${this._disableNewEntitiesChanged}
|
||||||
.disabled=${this._submitting}
|
.disabled=${this._submitting}
|
||||||
dialogInitialFocus
|
|
||||||
></ha-switch>
|
></ha-switch>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
|
|
||||||
@@ -113,9 +122,11 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
.disabled=${this._submitting}
|
.disabled=${this._submitting}
|
||||||
></ha-switch>
|
></ha-switch>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
|
|
||||||
|
<ha-dialog-footer slot="footer">
|
||||||
<ha-button
|
<ha-button
|
||||||
appearance="plain"
|
appearance="plain"
|
||||||
slot="primaryAction"
|
slot="secondaryAction"
|
||||||
@click=${this.closeDialog}
|
@click=${this.closeDialog}
|
||||||
.disabled=${this._submitting}
|
.disabled=${this._submitting}
|
||||||
>
|
>
|
||||||
@@ -126,9 +137,12 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
@click=${this._updateEntry}
|
@click=${this._updateEntry}
|
||||||
.disabled=${this._submitting}
|
.disabled=${this._submitting}
|
||||||
>
|
>
|
||||||
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.config_entry_system_options.update"
|
||||||
|
)}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</ha-dialog>
|
</ha-dialog-footer>
|
||||||
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import {
|
|||||||
mdiPlus,
|
mdiPlus,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import type {
|
||||||
|
SingleHassServiceTarget,
|
||||||
|
UnsubscribeFunc,
|
||||||
|
} from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import {
|
import {
|
||||||
@@ -21,15 +24,21 @@ import { repeat } from "lit/directives/repeat";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { tinykeys } from "tinykeys";
|
import { tinykeys } from "tinykeys";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||||
|
import { computeDeviceName } from "../../../common/entity/compute_device_name";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display";
|
||||||
|
import { computeFloorName } from "../../../common/entity/compute_floor_name";
|
||||||
import { stringCompare } from "../../../common/string/compare";
|
import { stringCompare } from "../../../common/string/compare";
|
||||||
import type {
|
import type {
|
||||||
LocalizeFunc,
|
LocalizeFunc,
|
||||||
LocalizeKeys,
|
LocalizeKeys,
|
||||||
} from "../../../common/translations/localize";
|
} from "../../../common/translations/localize";
|
||||||
|
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||||
import { debounce } from "../../../common/util/debounce";
|
import { debounce } from "../../../common/util/debounce";
|
||||||
import { deepEqual } from "../../../common/util/deep-equal";
|
import { deepEqual } from "../../../common/util/deep-equal";
|
||||||
import "../../../components/ha-bottom-sheet";
|
import "../../../components/ha-bottom-sheet";
|
||||||
|
import "../../../components/ha-button";
|
||||||
import "../../../components/ha-button-toggle-group";
|
import "../../../components/ha-button-toggle-group";
|
||||||
import "../../../components/ha-dialog-header";
|
import "../../../components/ha-dialog-header";
|
||||||
import "../../../components/ha-domain-icon";
|
import "../../../components/ha-domain-icon";
|
||||||
@@ -40,6 +49,7 @@ import "../../../components/ha-md-divider";
|
|||||||
import "../../../components/ha-md-list";
|
import "../../../components/ha-md-list";
|
||||||
import type { HaMdList } from "../../../components/ha-md-list";
|
import type { HaMdList } from "../../../components/ha-md-list";
|
||||||
import "../../../components/ha-md-list-item";
|
import "../../../components/ha-md-list-item";
|
||||||
|
import "../../../components/ha-section-title";
|
||||||
import "../../../components/ha-service-icon";
|
import "../../../components/ha-service-icon";
|
||||||
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
|
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
|
||||||
import "../../../components/ha-wa-dialog";
|
import "../../../components/ha-wa-dialog";
|
||||||
@@ -49,6 +59,10 @@ import {
|
|||||||
ACTION_COLLECTIONS,
|
ACTION_COLLECTIONS,
|
||||||
ACTION_ICONS,
|
ACTION_ICONS,
|
||||||
} from "../../../data/action";
|
} from "../../../data/action";
|
||||||
|
import {
|
||||||
|
getAreaDeviceLookup,
|
||||||
|
getAreaEntityLookup,
|
||||||
|
} from "../../../data/area_registry";
|
||||||
import {
|
import {
|
||||||
DYNAMIC_PREFIX,
|
DYNAMIC_PREFIX,
|
||||||
getValueFromDynamic,
|
getValueFromDynamic,
|
||||||
@@ -61,6 +75,8 @@ import {
|
|||||||
CONDITION_COLLECTIONS,
|
CONDITION_COLLECTIONS,
|
||||||
CONDITION_ICONS,
|
CONDITION_ICONS,
|
||||||
} from "../../../data/condition";
|
} from "../../../data/condition";
|
||||||
|
import { getDeviceEntityLookup } from "../../../data/device_registry";
|
||||||
|
import { getFloorAreaLookup } from "../../../data/floor_registry";
|
||||||
import { getServiceIcons, getTriggerIcons } from "../../../data/icons";
|
import { getServiceIcons, getTriggerIcons } from "../../../data/icons";
|
||||||
import type { IntegrationManifest } from "../../../data/integration";
|
import type { IntegrationManifest } from "../../../data/integration";
|
||||||
import {
|
import {
|
||||||
@@ -80,6 +96,8 @@ import { HaFuse } from "../../../resources/fuse";
|
|||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { isMac } from "../../../util/is_mac";
|
import { isMac } from "../../../util/is_mac";
|
||||||
import { showToast } from "../../../util/toast";
|
import { showToast } from "../../../util/toast";
|
||||||
|
import "./add-automation-element/ha-automation-add-from-target";
|
||||||
|
import type HaAutomationAddFromTarget from "./add-automation-element/ha-automation-add-from-target";
|
||||||
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
||||||
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
||||||
|
|
||||||
@@ -134,7 +152,9 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
@state() private _selectedGroup?: string;
|
@state() private _selectedGroup?: string;
|
||||||
|
|
||||||
@state() private _tab: "groups" | "blocks" = "groups";
|
@state() private _selectedTarget?: SingleHassServiceTarget;
|
||||||
|
|
||||||
|
@state() private _tab: "targets" | "groups" | "blocks" = "targets";
|
||||||
|
|
||||||
@state() private _filter = "";
|
@state() private _filter = "";
|
||||||
|
|
||||||
@@ -150,20 +170,60 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
@state() private _narrow = false;
|
@state() private _narrow = false;
|
||||||
|
|
||||||
|
@state() private _showTargetShowMoreButton?: boolean;
|
||||||
|
|
||||||
|
@state() private _targetPickerFullHeight = false;
|
||||||
|
|
||||||
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
||||||
|
|
||||||
@query(".items ha-md-list ha-md-list-item")
|
@query(".items ha-md-list ha-md-list-item")
|
||||||
private _itemsListFirstElement?: HaMdList;
|
private _itemsListFirstElement?: HaMdList;
|
||||||
|
|
||||||
|
@query("ha-automation-add-from-target")
|
||||||
|
private _targetPickerElement?: HaAutomationAddFromTarget;
|
||||||
|
|
||||||
@query(".items")
|
@query(".items")
|
||||||
private _itemsListElement?: HTMLDivElement;
|
private _itemsListElement?: HTMLDivElement;
|
||||||
|
|
||||||
|
@query(".content")
|
||||||
|
private _contentElement?: HTMLDivElement;
|
||||||
|
|
||||||
private _fullScreen = false;
|
private _fullScreen = false;
|
||||||
|
|
||||||
private _removeKeyboardShortcuts?: () => void;
|
private _removeKeyboardShortcuts?: () => void;
|
||||||
|
|
||||||
private _unsub?: Promise<UnsubscribeFunc>;
|
private _unsub?: Promise<UnsubscribeFunc>;
|
||||||
|
|
||||||
|
protected willUpdate(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
this._params?.type === "action" &&
|
||||||
|
changedProps.has("hass") &&
|
||||||
|
changedProps.get("hass")?.states !== this.hass.states
|
||||||
|
) {
|
||||||
|
this._calculateUsedDomains();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
changedProps.has("_tab") ||
|
||||||
|
changedProps.has("_selectedTarget") ||
|
||||||
|
changedProps.has("_narrow")
|
||||||
|
) {
|
||||||
|
this._targetPickerFullHeight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
(changedProps.has("_tab") ||
|
||||||
|
changedProps.has("_selectedTarget") ||
|
||||||
|
changedProps.has("_narrow") ||
|
||||||
|
this._showTargetShowMoreButton === undefined) &&
|
||||||
|
this._narrow
|
||||||
|
) {
|
||||||
|
this._setShowTargetShowMoreButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public showDialog(params): void {
|
public showDialog(params): void {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
|
|
||||||
@@ -211,7 +271,8 @@ class DialogAddAutomationElement
|
|||||||
this._bottomSheetMode = false;
|
this._bottomSheetMode = false;
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
this._selectedGroup = undefined;
|
this._selectedGroup = undefined;
|
||||||
this._tab = "groups";
|
this._tab = "targets";
|
||||||
|
this._selectedTarget = undefined;
|
||||||
this._selectedCollectionIndex = undefined;
|
this._selectedCollectionIndex = undefined;
|
||||||
this._filter = "";
|
this._filter = "";
|
||||||
this._manifests = undefined;
|
this._manifests = undefined;
|
||||||
@@ -781,23 +842,14 @@ class DialogAddAutomationElement
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
|
||||||
if (
|
|
||||||
this._params?.type === "action" &&
|
|
||||||
changedProperties.has("hass") &&
|
|
||||||
changedProperties.get("hass")?.states !== this.hass.states
|
|
||||||
) {
|
|
||||||
this._calculateUsedDomains();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderContent() {
|
private _renderContent() {
|
||||||
const automationElementType = this._params!.type;
|
const automationElementType = this._params!.type;
|
||||||
|
|
||||||
const items = this._filter
|
const items =
|
||||||
|
this._filter || this._selectedTarget
|
||||||
? this._getFilteredItems(
|
? this._getFilteredItems(
|
||||||
automationElementType,
|
automationElementType,
|
||||||
this._filter,
|
this._filter || "Ti",
|
||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this.hass.services,
|
this.hass.services,
|
||||||
this._manifests
|
this._manifests
|
||||||
@@ -835,35 +887,25 @@ class DialogAddAutomationElement
|
|||||||
this._manifests
|
this._manifests
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupName = isDynamic(this._selectedGroup)
|
|
||||||
? domainToName(
|
|
||||||
this.hass.localize,
|
|
||||||
getValueFromDynamic(this._selectedGroup!),
|
|
||||||
this._manifests?.[getValueFromDynamic(this._selectedGroup!)]
|
|
||||||
)
|
|
||||||
: this.hass.localize(
|
|
||||||
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys
|
|
||||||
) ||
|
|
||||||
this.hass.localize(
|
|
||||||
`ui.panel.config.automation.editor.${this._params!.type}s.type.${this._selectedGroup}.label` as LocalizeKeys
|
|
||||||
);
|
|
||||||
|
|
||||||
const typeTitle = this.hass.localize(
|
|
||||||
`ui.panel.config.automation.editor.${automationElementType}s.add`
|
|
||||||
);
|
|
||||||
|
|
||||||
const tabButtons = [
|
const tabButtons = [
|
||||||
|
{
|
||||||
|
label: this.hass.localize(`ui.panel.config.automation.editor.targets`),
|
||||||
|
value: "targets",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: this.hass.localize(
|
label: this.hass.localize(
|
||||||
`ui.panel.config.automation.editor.${automationElementType}s.name`
|
`ui.panel.config.automation.editor.${automationElementType}s.name`
|
||||||
),
|
),
|
||||||
value: "groups",
|
value: "groups",
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (this._params?.type !== "trigger") {
|
||||||
|
tabButtons.push({
|
||||||
label: this.hass.localize(`ui.panel.config.automation.editor.blocks`),
|
label: this.hass.localize(`ui.panel.config.automation.editor.blocks`),
|
||||||
value: "blocks",
|
value: "blocks",
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
const hideCollections =
|
const hideCollections =
|
||||||
this._filter ||
|
this._filter ||
|
||||||
@@ -872,28 +914,8 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div slot="header">
|
<div slot="header">
|
||||||
<ha-dialog-header subtitle-position="above">
|
${this._renderHeader()}
|
||||||
<span slot="title"
|
${!this._narrow || (!this._selectedGroup && !this._selectedTarget)
|
||||||
>${this._narrow && this._selectedGroup
|
|
||||||
? groupName
|
|
||||||
: typeTitle}</span
|
|
||||||
>
|
|
||||||
|
|
||||||
${this._narrow && this._selectedGroup
|
|
||||||
? html`<span slot="subtitle">${typeTitle}</span>`
|
|
||||||
: nothing}
|
|
||||||
${this._narrow && this._selectedGroup
|
|
||||||
? html`<ha-icon-button-prev
|
|
||||||
slot="navigationIcon"
|
|
||||||
@click=${this._back}
|
|
||||||
></ha-icon-button-prev>`
|
|
||||||
: html`<ha-icon-button
|
|
||||||
.path=${mdiClose}
|
|
||||||
@click=${this._close}
|
|
||||||
slot="navigationIcon"
|
|
||||||
></ha-icon-button>`}
|
|
||||||
</ha-dialog-header>
|
|
||||||
${!this._narrow || !this._selectedGroup
|
|
||||||
? html`
|
? html`
|
||||||
<search-input
|
<search-input
|
||||||
?autofocus=${!this._narrow}
|
?autofocus=${!this._narrow}
|
||||||
@@ -906,9 +928,8 @@ class DialogAddAutomationElement
|
|||||||
></search-input>
|
></search-input>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
${this._params?.type !== "trigger" &&
|
${!this._filter &&
|
||||||
!this._filter &&
|
(!this._narrow || (!this._selectedGroup && !this._selectedTarget))
|
||||||
(!this._narrow || !this._selectedGroup)
|
|
||||||
? html`<ha-button-toggle-group
|
? html`<ha-button-toggle-group
|
||||||
variant="neutral"
|
variant="neutral"
|
||||||
active-variant="brand"
|
active-variant="brand"
|
||||||
@@ -920,7 +941,37 @@ class DialogAddAutomationElement
|
|||||||
></ha-button-toggle-group>`
|
></ha-button-toggle-group>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div
|
||||||
|
class=${classMap({
|
||||||
|
content: true,
|
||||||
|
column: this._narrow && this._selectedTarget,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${this._tab === "targets"
|
||||||
|
? html`<ha-automation-add-from-target
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this._selectedTarget}
|
||||||
|
@value-changed=${this._handleTargetSelected}
|
||||||
|
.narrow=${this._narrow}
|
||||||
|
class=${this._getAddFromTargetHidden()}
|
||||||
|
></ha-automation-add-from-target>
|
||||||
|
${this._narrow &&
|
||||||
|
this._showTargetShowMoreButton &&
|
||||||
|
!this._targetPickerFullHeight
|
||||||
|
? html`
|
||||||
|
<div class="targets-show-more">
|
||||||
|
<ha-button
|
||||||
|
appearance="plain"
|
||||||
|
@click=${this._expandTargetList}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.automation.editor.show_more`
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing} `
|
||||||
|
: html`
|
||||||
<ha-md-list
|
<ha-md-list
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
groups: true,
|
groups: true,
|
||||||
@@ -976,14 +1027,17 @@ class DialogAddAutomationElement
|
|||||||
.path=${mdiPlus}
|
.path=${mdiPlus}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
<ha-md-divider
|
||||||
|
role="separator"
|
||||||
|
tabindex="-1"
|
||||||
|
></ha-md-divider>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${collections.map(
|
${collections.map(
|
||||||
(collection, index) => html`
|
(collection, index) => html`
|
||||||
${collection.titleKey
|
${collection.titleKey
|
||||||
? html`<div class="collection-title">
|
? html`<ha-section-title>
|
||||||
${this.hass.localize(collection.titleKey)}
|
${this.hass.localize(collection.titleKey)}
|
||||||
</div>`
|
</ha-section-title>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${repeat(
|
${repeat(
|
||||||
collection.groups,
|
collection.groups,
|
||||||
@@ -995,7 +1049,9 @@ class DialogAddAutomationElement
|
|||||||
.value=${item.key}
|
.value=${item.key}
|
||||||
.index=${index}
|
.index=${index}
|
||||||
@click=${this._groupSelected}
|
@click=${this._groupSelected}
|
||||||
class=${item.key === this._selectedGroup ? "selected" : ""}
|
class=${item.key === this._selectedGroup
|
||||||
|
? "selected"
|
||||||
|
: ""}
|
||||||
>
|
>
|
||||||
<div slot="headline">${item.name}</div>
|
<div slot="headline">${item.name}</div>
|
||||||
${item.icon
|
${item.icon
|
||||||
@@ -1012,18 +1068,27 @@ class DialogAddAutomationElement
|
|||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</ha-md-list>
|
</ha-md-list>
|
||||||
|
`}
|
||||||
<div
|
<div
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
items: true,
|
items: true,
|
||||||
blank:
|
blank:
|
||||||
!this._selectedGroup && !this._filter && this._tab === "groups",
|
(this._tab === "groups" &&
|
||||||
|
!this._selectedGroup &&
|
||||||
|
!this._filter) ||
|
||||||
|
(this._tab === "targets" &&
|
||||||
|
!this._selectedTarget &&
|
||||||
|
!this._filter),
|
||||||
"empty-search":
|
"empty-search":
|
||||||
!items?.length && !filteredBlockItems?.length && this._filter,
|
!items?.length && !filteredBlockItems?.length && this._filter,
|
||||||
hidden:
|
hidden:
|
||||||
this._narrow &&
|
this._narrow &&
|
||||||
!this._selectedGroup &&
|
!this._selectedGroup &&
|
||||||
|
(!this._selectedTarget ||
|
||||||
|
(this._selectedTarget &&
|
||||||
|
!Object.values(this._selectedTarget)[0])) &&
|
||||||
!this._filter &&
|
!this._filter &&
|
||||||
this._tab === "groups",
|
this._tab !== "blocks",
|
||||||
})}
|
})}
|
||||||
@scroll=${this._onItemsScroll}
|
@scroll=${this._onItemsScroll}
|
||||||
>
|
>
|
||||||
@@ -1037,6 +1102,10 @@ class DialogAddAutomationElement
|
|||||||
? this.hass.localize(
|
? this.hass.localize(
|
||||||
`ui.panel.config.automation.editor.${automationElementType}s.select`
|
`ui.panel.config.automation.editor.${automationElementType}s.select`
|
||||||
)
|
)
|
||||||
|
: this._tab === "targets" && !this._selectedTarget && !this._filter
|
||||||
|
? this.hass.localize(
|
||||||
|
`ui.panel.config.automation.editor.select_target`
|
||||||
|
)
|
||||||
: !items?.length &&
|
: !items?.length &&
|
||||||
this._filter &&
|
this._filter &&
|
||||||
(!filteredBlockItems || !filteredBlockItems.length)
|
(!filteredBlockItems || !filteredBlockItems.length)
|
||||||
@@ -1149,6 +1218,10 @@ class DialogAddAutomationElement
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _back() {
|
private _back() {
|
||||||
|
if (this._selectedTarget) {
|
||||||
|
this._targetPickerElement?.navigateBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._selectedGroup = undefined;
|
this._selectedGroup = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1172,6 +1245,21 @@ class DialogAddAutomationElement
|
|||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleTargetSelected = (
|
||||||
|
ev: CustomEvent<{ value: SingleHassServiceTarget }>
|
||||||
|
) => {
|
||||||
|
this._selectedTarget = ev.detail.value;
|
||||||
|
|
||||||
|
// TODO fix on mobile
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (this._narrow) {
|
||||||
|
this._contentElement?.scrollTo(0, 0);
|
||||||
|
} else {
|
||||||
|
this._itemsListElement?.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private _debounceFilterChanged = debounce(
|
private _debounceFilterChanged = debounce(
|
||||||
(ev) => this._filterChanged(ev),
|
(ev) => this._filterChanged(ev),
|
||||||
200
|
200
|
||||||
@@ -1268,6 +1356,230 @@ class DialogAddAutomationElement
|
|||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _renderHeader() {
|
||||||
|
return html`
|
||||||
|
<ha-dialog-header subtitle-position="above">
|
||||||
|
<span slot="title">${this._getDialogTitle()}</span>
|
||||||
|
|
||||||
|
${this._renderDialogSubtitle()}
|
||||||
|
${this._narrow && (this._selectedGroup || this._selectedTarget)
|
||||||
|
? html`<ha-icon-button-prev
|
||||||
|
slot="navigationIcon"
|
||||||
|
@click=${this._back}
|
||||||
|
></ha-icon-button-prev>`
|
||||||
|
: html`<ha-icon-button
|
||||||
|
.path=${mdiClose}
|
||||||
|
@click=${this._close}
|
||||||
|
slot="navigationIcon"
|
||||||
|
></ha-icon-button>`}
|
||||||
|
</ha-dialog-header>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getDialogTitle() {
|
||||||
|
if (this._narrow && this._selectedGroup) {
|
||||||
|
return isDynamic(this._selectedGroup)
|
||||||
|
? domainToName(
|
||||||
|
this.hass.localize,
|
||||||
|
getValueFromDynamic(this._selectedGroup!),
|
||||||
|
this._manifests?.[getValueFromDynamic(this._selectedGroup!)]
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys
|
||||||
|
) ||
|
||||||
|
this.hass.localize(
|
||||||
|
`ui.panel.config.automation.editor.${this._params!.type}s.type.${this._selectedGroup}.label` as LocalizeKeys
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._narrow && this._selectedTarget) {
|
||||||
|
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
|
||||||
|
this._selectedTarget
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetId === undefined && targetType === "floor") {
|
||||||
|
return this.hass.localize("ui.components.area-picker.unassigned_areas");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId === undefined && targetType === "area") {
|
||||||
|
return this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.unassigned_devices"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId === undefined && targetType === "device") {
|
||||||
|
return this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.unassigned_entities"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId) {
|
||||||
|
if (targetType === "floor") {
|
||||||
|
return computeFloorName(this.hass.floors[targetId]) || targetId;
|
||||||
|
}
|
||||||
|
if (targetType === "area") {
|
||||||
|
return computeAreaName(this.hass.areas[targetId]) || targetId;
|
||||||
|
}
|
||||||
|
if (targetType === "device") {
|
||||||
|
return computeDeviceName(this.hass.devices[targetId]) || targetId;
|
||||||
|
}
|
||||||
|
if (targetType === "entity" && this.hass.states[targetId]) {
|
||||||
|
const stateObj = this.hass.states[targetId];
|
||||||
|
const [entityName, deviceName] = computeEntityNameList(
|
||||||
|
stateObj,
|
||||||
|
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||||
|
this.hass.entities,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.floors
|
||||||
|
);
|
||||||
|
|
||||||
|
return entityName || deviceName || targetId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hass.localize(
|
||||||
|
`ui.panel.config.automation.editor.${this._params!.type}s.add`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderDialogSubtitle() {
|
||||||
|
if (!this._narrow) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._selectedGroup) {
|
||||||
|
return html`<span slot="subtitle"
|
||||||
|
>${this.hass.localize(
|
||||||
|
`ui.panel.config.automation.editor.${this._params!.type}s.add`
|
||||||
|
)}</span
|
||||||
|
>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._selectedTarget) {
|
||||||
|
let subtitle: string | undefined;
|
||||||
|
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
|
||||||
|
this._selectedTarget
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetId) {
|
||||||
|
if (targetType === "area" && this.hass.areas[targetId].floor_id) {
|
||||||
|
const floorId = this.hass.areas[targetId].floor_id;
|
||||||
|
subtitle = computeFloorName(this.hass.floors[floorId]) || floorId;
|
||||||
|
}
|
||||||
|
if (targetType === "device" && this.hass.devices[targetId].area_id) {
|
||||||
|
const areaId = this.hass.devices[targetId].area_id;
|
||||||
|
subtitle = computeAreaName(this.hass.areas[areaId]) || areaId;
|
||||||
|
}
|
||||||
|
if (targetType === "entity" && this.hass.states[targetId]) {
|
||||||
|
const stateObj = this.hass.states[targetId];
|
||||||
|
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||||
|
stateObj,
|
||||||
|
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||||
|
this.hass.entities,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.floors
|
||||||
|
);
|
||||||
|
|
||||||
|
subtitle = [areaName, entityName ? deviceName : undefined]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subtitle) {
|
||||||
|
return html`<span slot="subtitle">${subtitle}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getFloorAreaLookupMemoized = memoizeOne(
|
||||||
|
(areas: HomeAssistant["areas"]) => getFloorAreaLookup(Object.values(areas))
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getAreaDeviceLookupMemoized = memoizeOne(
|
||||||
|
(devices: HomeAssistant["devices"]) =>
|
||||||
|
getAreaDeviceLookup(Object.values(devices))
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getAreaEntityLookupMemoized = memoizeOne(
|
||||||
|
(entities: HomeAssistant["entities"]) =>
|
||||||
|
getAreaEntityLookup(Object.values(entities), true)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getDeviceEntityLookupMemoized = memoizeOne(
|
||||||
|
(entities: HomeAssistant["entities"]) =>
|
||||||
|
getDeviceEntityLookup(Object.values(entities), true)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _extractTypeAndIdFromTarget = memoizeOne(
|
||||||
|
(target: SingleHassServiceTarget): [string, string | undefined] => {
|
||||||
|
const [targetTypeId, targetId] = Object.entries(target)[0];
|
||||||
|
const targetType = targetTypeId.replace("_id", "");
|
||||||
|
return [targetType, targetId];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getAddFromTargetHidden() {
|
||||||
|
const classes: string[] = [];
|
||||||
|
if (this._narrow && this._selectedTarget) {
|
||||||
|
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
|
||||||
|
this._selectedTarget
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
targetId &&
|
||||||
|
((targetType === "floor" &&
|
||||||
|
!(
|
||||||
|
this._getFloorAreaLookupMemoized(this.hass.areas)[targetId]
|
||||||
|
?.length > 0
|
||||||
|
)) ||
|
||||||
|
(targetType === "area" &&
|
||||||
|
!(
|
||||||
|
this._getAreaDeviceLookupMemoized(this.hass.devices)[targetId]
|
||||||
|
?.length > 0
|
||||||
|
) &&
|
||||||
|
!(
|
||||||
|
this._getAreaEntityLookupMemoized(this.hass.entities)[targetId]
|
||||||
|
?.length > 0
|
||||||
|
)) ||
|
||||||
|
(targetType === "device" &&
|
||||||
|
!(
|
||||||
|
this._getDeviceEntityLookupMemoized(this.hass.entities)[targetId]
|
||||||
|
?.length > 0
|
||||||
|
)) ||
|
||||||
|
targetType === "entity")
|
||||||
|
) {
|
||||||
|
classes.push("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._selectedTarget && Object.values(this._selectedTarget)[0]) {
|
||||||
|
classes.push("selected-target");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._targetPickerFullHeight) {
|
||||||
|
classes.push("full-height");
|
||||||
|
}
|
||||||
|
return classes.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _setShowTargetShowMoreButton() {
|
||||||
|
await this._targetPickerElement?.updateComplete;
|
||||||
|
this._showTargetShowMoreButton =
|
||||||
|
this._targetPickerElement &&
|
||||||
|
this._targetPickerElement.scrollHeight >
|
||||||
|
this._targetPickerElement.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandTargetList() {
|
||||||
|
this._targetPickerFullHeight = true;
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
css`
|
css`
|
||||||
@@ -1321,10 +1633,15 @@ class DialogAddAutomationElement
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content.column {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
ha-md-list {
|
ha-md-list {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-automation-add-from-target,
|
||||||
.groups {
|
.groups {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
flex: 3;
|
flex: 3;
|
||||||
@@ -1332,14 +1649,48 @@ class DialogAddAutomationElement
|
|||||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||||
margin: var(--ha-space-3);
|
margin: var(--ha-space-3);
|
||||||
margin-inline-end: var(--ha-space-0);
|
margin-inline-end: var(--ha-space-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.targets-show-more {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: calc(100% - var(--ha-space-6));
|
||||||
|
padding-bottom: var(--ha-space-2);
|
||||||
|
box-shadow: inset var(--ha-shadow-offset-x-lg)
|
||||||
|
calc(var(--ha-shadow-offset-y-lg) * -1) var(--ha-shadow-blur-lg)
|
||||||
|
var(--ha-shadow-spread-lg) var(--ha-color-shadow-light);
|
||||||
|
margin: 0 var(--ha-space-3);
|
||||||
|
border-end-end-radius: var(--ha-border-radius-xl);
|
||||||
|
border-end-start-radius: var(--ha-border-radius-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 870px), all and (max-height: 500px) {
|
||||||
|
ha-automation-add-from-target.selected-target {
|
||||||
|
max-height: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-automation-add-from-target.full-height {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-automation-add-from-target.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groups {
|
||||||
--md-list-item-leading-space: var(--ha-space-3);
|
--md-list-item-leading-space: var(--ha-space-3);
|
||||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||||
--md-list-item-bottom-space: var(--ha-space-1);
|
--md-list-item-bottom-space: var(--ha-space-1);
|
||||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
--md-list-item-supporting-text-font: var(--ha-font-family-body);
|
||||||
--md-list-item-one-line-container-height: var(--ha-space-10);
|
--md-list-item-one-line-container-height: var(--ha-space-10);
|
||||||
}
|
}
|
||||||
ha-bottom-sheet .groups {
|
ha-bottom-sheet .groups,
|
||||||
|
ha-bottom-sheet ha-automation-add-from-target {
|
||||||
margin: var(--ha-space-3);
|
margin: var(--ha-space-3);
|
||||||
}
|
}
|
||||||
.groups .selected {
|
.groups .selected {
|
||||||
@@ -1351,16 +1702,9 @@ class DialogAddAutomationElement
|
|||||||
color: var(--ha-color-on-primary-normal);
|
color: var(--ha-color-on-primary-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.collection-title {
|
ha-section-title {
|
||||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
|
||||||
padding: var(--ha-space-1) var(--ha-space-2);
|
|
||||||
font-weight: var(--ha-font-weight-bold);
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
top: 0;
|
top: 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
min-height: var(--ha-space-6);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1371,6 +1715,11 @@ class DialogAddAutomationElement
|
|||||||
flex: 7;
|
flex: 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content.column ha-automation-add-from-target,
|
||||||
|
.content.column .items {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
ha-wa-dialog .items {
|
ha-wa-dialog .items {
|
||||||
margin-top: var(--ha-space-3);
|
margin-top: var(--ha-space-3);
|
||||||
}
|
}
|
||||||
@@ -1400,7 +1749,7 @@ class DialogAddAutomationElement
|
|||||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||||
--md-list-item-bottom-space: var(--ha-space-2);
|
--md-list-item-bottom-space: var(--ha-space-2);
|
||||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
--md-list-item-supporting-text-font: var(--ha-font-family-body);
|
||||||
gap: var(--ha-space-2);
|
gap: var(--ha-space-2);
|
||||||
padding: var(--ha-space-0) var(--ha-space-4);
|
padding: var(--ha-space-0) var(--ha-space-4);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,974 @@
|
|||||||
|
import "@home-assistant/webawesome/dist/components/tree-item/tree-item";
|
||||||
|
import "@home-assistant/webawesome/dist/components/tree/tree";
|
||||||
|
import type { WaSelectionChangeEvent } from "@home-assistant/webawesome/dist/events/selection-change";
|
||||||
|
import { consume } from "@lit/context";
|
||||||
|
import {
|
||||||
|
mdiCubeOutline,
|
||||||
|
mdiDevices,
|
||||||
|
mdiSelectionMarker,
|
||||||
|
mdiTextureBox,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import type { SingleHassServiceTarget } from "home-assistant-js-websocket";
|
||||||
|
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||||
|
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||||
|
import "../../../../components/entity/state-badge";
|
||||||
|
import "../../../../components/ha-floor-icon";
|
||||||
|
import "../../../../components/ha-icon";
|
||||||
|
import "../../../../components/ha-icon-next";
|
||||||
|
import "../../../../components/ha-md-list";
|
||||||
|
import "../../../../components/ha-md-list-item";
|
||||||
|
import "../../../../components/ha-section-title";
|
||||||
|
import "../../../../components/ha-svg-icon";
|
||||||
|
import {
|
||||||
|
getAreasNestedInFloors,
|
||||||
|
type AreaFloorValue,
|
||||||
|
type FloorComboBoxItem,
|
||||||
|
type FloorNestedComboBoxItem,
|
||||||
|
type UnassignedAreasFloorComboBoxItem,
|
||||||
|
} from "../../../../data/area_floor";
|
||||||
|
import {
|
||||||
|
getConfigEntries,
|
||||||
|
type ConfigEntry,
|
||||||
|
} from "../../../../data/config_entries";
|
||||||
|
import {
|
||||||
|
areasContext,
|
||||||
|
devicesContext,
|
||||||
|
entitiesContext,
|
||||||
|
floorsContext,
|
||||||
|
labelsContext,
|
||||||
|
localizeContext,
|
||||||
|
statesContext,
|
||||||
|
} from "../../../../data/context";
|
||||||
|
import {
|
||||||
|
getLabels,
|
||||||
|
type LabelRegistryEntry,
|
||||||
|
} from "../../../../data/label_registry";
|
||||||
|
import { extractFromTarget } from "../../../../data/target";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { brandsUrl } from "../../../../util/brands-url";
|
||||||
|
|
||||||
|
const SEPARATOR = "________";
|
||||||
|
|
||||||
|
interface DeviceEntries {
|
||||||
|
open: boolean;
|
||||||
|
entities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AreaEntries {
|
||||||
|
open: boolean;
|
||||||
|
devices: Record<string, DeviceEntries>;
|
||||||
|
entities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-automation-add-from-target")
|
||||||
|
export default class HaAutomationAddFromTarget extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public value?: SingleHassServiceTarget;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
// #region context
|
||||||
|
@state()
|
||||||
|
@consume({ context: localizeContext, subscribe: true })
|
||||||
|
private localize!: HomeAssistant["localize"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: statesContext, subscribe: true })
|
||||||
|
private states!: HomeAssistant["states"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: floorsContext, subscribe: true })
|
||||||
|
private floors!: HomeAssistant["floors"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: areasContext, subscribe: true })
|
||||||
|
private areas!: HomeAssistant["areas"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: devicesContext, subscribe: true })
|
||||||
|
private devices!: HomeAssistant["devices"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: entitiesContext, subscribe: true })
|
||||||
|
private entities!: HomeAssistant["entities"];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: labelsContext, subscribe: true })
|
||||||
|
private _labelRegistry!: LabelRegistryEntry[];
|
||||||
|
// #endregion context
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _floorAreas: (
|
||||||
|
| FloorNestedComboBoxItem
|
||||||
|
| UnassignedAreasFloorComboBoxItem
|
||||||
|
)[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _areaEntries: Record<string, AreaEntries> = {};
|
||||||
|
|
||||||
|
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||||
|
|
||||||
|
private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
this._loadConfigEntries();
|
||||||
|
this._getTreeData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
${!this.narrow || !this.value ? this._renderFloors() : nothing}
|
||||||
|
${this.narrow && this.value ? this._renderNarrow(this.value) : nothing}
|
||||||
|
${!this.narrow || !this.value ? this._renderLabels() : nothing}
|
||||||
|
${!this.narrow || !this.value ? this._renderUnassigned() : nothing}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderNarrow(value: SingleHassServiceTarget) {
|
||||||
|
const [valueTypeId, valueId] = Object.entries(value)[0];
|
||||||
|
const valueType = valueTypeId.replace("_id", "");
|
||||||
|
|
||||||
|
if (!valueType || valueType === "label") {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "floor") {
|
||||||
|
return this._renderAreas(
|
||||||
|
this._floorAreas.find(
|
||||||
|
(floor) =>
|
||||||
|
(valueId && floor.id === `${valueType}${SEPARATOR}${valueId}`) ||
|
||||||
|
(!valueId && !floor.id)
|
||||||
|
)?.areas
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "area") {
|
||||||
|
const { devices, entities } =
|
||||||
|
this._areaEntries[`area${SEPARATOR}${valueId ?? ""}`];
|
||||||
|
const numberOfDevices = Object.keys(devices).length;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${numberOfDevices ? this._renderDevices(devices) : nothing}
|
||||||
|
${entities.length ? this._renderEntities(entities) : nothing}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "device" && this.devices[valueId]) {
|
||||||
|
const deviceArea = this.devices[valueId].area_id!;
|
||||||
|
return this._renderEntities(
|
||||||
|
this._areaEntries[`area${SEPARATOR}${deviceArea}`]?.devices[valueId]
|
||||||
|
?.entities
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (valueType === "device" && !valueId) {
|
||||||
|
return this._renderEntities(this._getUnassignedEntities(this.entities));
|
||||||
|
}
|
||||||
|
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderFloors() {
|
||||||
|
return html`<ha-section-title
|
||||||
|
>${this.localize(
|
||||||
|
"ui.panel.config.automation.editor.home"
|
||||||
|
)}</ha-section-title
|
||||||
|
>
|
||||||
|
${!this._floorAreas.length ||
|
||||||
|
(!this._floorAreas[0].id && !this._floorAreas[0].areas.length)
|
||||||
|
? html`<ha-md-list>
|
||||||
|
<ha-md-list-item type="text">
|
||||||
|
<div slot="headline">
|
||||||
|
${this.localize("ui.components.area-picker.no_areas")}
|
||||||
|
</div>
|
||||||
|
</ha-md-list-item>
|
||||||
|
</ha-md-list>`
|
||||||
|
: this.narrow
|
||||||
|
? html`<ha-md-list>
|
||||||
|
${this._floorAreas.map((floor, index) =>
|
||||||
|
index === 0 && !floor.id
|
||||||
|
? this._renderAreas(floor.areas)
|
||||||
|
: !floor.id
|
||||||
|
? nothing
|
||||||
|
: html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${floor.id || `floor${SEPARATOR}`}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
>
|
||||||
|
${floor.id && (floor as FloorNestedComboBoxItem).floor
|
||||||
|
? html`<ha-floor-icon
|
||||||
|
slot="start"
|
||||||
|
.floor=${(floor as FloorNestedComboBoxItem).floor}
|
||||||
|
></ha-floor-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${mdiSelectionMarker}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
|
||||||
|
<div slot="headline">
|
||||||
|
${!floor.id
|
||||||
|
? this.localize(
|
||||||
|
"ui.components.area-picker.unassigned_areas"
|
||||||
|
)
|
||||||
|
: floor.primary}
|
||||||
|
</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>`
|
||||||
|
)}
|
||||||
|
</ha-md-list>`
|
||||||
|
: html`<wa-tree @wa-selection-change=${this._handleSelectionChange}>
|
||||||
|
${this._floorAreas.map((floor, index) =>
|
||||||
|
index === 0 && !floor.id
|
||||||
|
? this._renderAreas(floor.areas)
|
||||||
|
: !floor.id
|
||||||
|
? nothing
|
||||||
|
: html`<wa-tree-item
|
||||||
|
.disabledSelection=${!floor.id}
|
||||||
|
.target=${floor.id}
|
||||||
|
.selected=${!!floor.id &&
|
||||||
|
this._getSelectedTargetId(this.value) === floor.id}
|
||||||
|
>
|
||||||
|
${floor.id && (floor as FloorNestedComboBoxItem).floor
|
||||||
|
? html`<ha-floor-icon
|
||||||
|
.floor=${(floor as FloorNestedComboBoxItem).floor}
|
||||||
|
></ha-floor-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
.path=${mdiSelectionMarker}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
${!floor.id
|
||||||
|
? this.localize(
|
||||||
|
"ui.components.area-picker.unassigned_areas"
|
||||||
|
)
|
||||||
|
: floor.primary}
|
||||||
|
${this._renderAreas(floor.areas)}
|
||||||
|
</wa-tree-item>`
|
||||||
|
)}
|
||||||
|
</wa-tree>`} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderLabels() {
|
||||||
|
const labels = this._getLabelsMemoized(
|
||||||
|
this.states,
|
||||||
|
this.areas,
|
||||||
|
this.devices,
|
||||||
|
this.entities,
|
||||||
|
this._labelRegistry,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
`label${SEPARATOR}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!labels.length) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<ha-section-title
|
||||||
|
>${this.localize("ui.components.label-picker.labels")}</ha-section-title
|
||||||
|
>
|
||||||
|
<ha-md-list>
|
||||||
|
${labels.map(
|
||||||
|
(label) =>
|
||||||
|
html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${label.id}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
class=${this._getSelectedTargetId(this.value) === label.id
|
||||||
|
? "selected"
|
||||||
|
: ""}
|
||||||
|
>${label.icon
|
||||||
|
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
|
||||||
|
: label.icon_path
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${label.icon_path}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: nothing}
|
||||||
|
<div slot="headline">${label.primary}</div>
|
||||||
|
${this.narrow
|
||||||
|
? html`<ha-icon-next slot="end"></ha-icon-next> `
|
||||||
|
: nothing}
|
||||||
|
</ha-md-list-item>`
|
||||||
|
)}
|
||||||
|
</ha-md-list>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getUnassignedEntities = memoizeOne(
|
||||||
|
(entities: HomeAssistant["entities"]): string[] =>
|
||||||
|
Object.values(entities)
|
||||||
|
.filter((entity) => !entity.area_id && !entity.device_id)
|
||||||
|
.map(({ entity_id }) => entity_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _renderUnassigned() {
|
||||||
|
const unassignedAreas =
|
||||||
|
this._floorAreas.length > 1
|
||||||
|
? this._floorAreas.find((floor) => !floor.id)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const unassignedEntities = this._getUnassignedEntities(this.entities);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!unassignedAreas?.areas.length &&
|
||||||
|
!this._areaEntries[`area${SEPARATOR}`] &&
|
||||||
|
!Object.keys(this._areaEntries[`area${SEPARATOR}`]?.devices).length &&
|
||||||
|
!unassignedEntities.length
|
||||||
|
) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<ha-section-title
|
||||||
|
>${this.localize(
|
||||||
|
"ui.panel.config.automation.editor.unassigned"
|
||||||
|
)}</ha-section-title
|
||||||
|
>${this.narrow
|
||||||
|
? html`<ha-md-list>
|
||||||
|
${unassignedAreas?.areas.length
|
||||||
|
? html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${`floor${SEPARATOR}`}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${mdiTextureBox}
|
||||||
|
></ha-svg-icon>
|
||||||
|
<div slot="headline">
|
||||||
|
${this.localize("ui.components.target-picker.type.areas")}
|
||||||
|
</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>`
|
||||||
|
: nothing}
|
||||||
|
${this._areaEntries[`area${SEPARATOR}`] &&
|
||||||
|
Object.keys(this._areaEntries[`area${SEPARATOR}`]?.devices).length
|
||||||
|
? html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${`area${SEPARATOR}`}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||||
|
<div slot="headline">
|
||||||
|
${this.localize("ui.components.target-picker.type.devices")}
|
||||||
|
</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>`
|
||||||
|
: nothing}
|
||||||
|
${unassignedEntities.length
|
||||||
|
? html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${`device${SEPARATOR}`}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${mdiCubeOutline}
|
||||||
|
></ha-svg-icon>
|
||||||
|
<div slot="headline">
|
||||||
|
${this.localize(
|
||||||
|
"ui.components.target-picker.type.entities"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>`
|
||||||
|
: nothing}
|
||||||
|
</ha-md-list>`
|
||||||
|
: html`<wa-tree @wa-selection-change=${this._handleSelectionChange}>
|
||||||
|
${unassignedAreas?.areas.length
|
||||||
|
? html`<wa-tree-item disabled-selection>
|
||||||
|
<ha-svg-icon .path=${mdiSelectionMarker}></ha-svg-icon>
|
||||||
|
${this.localize("ui.components.target-picker.type.areas")}
|
||||||
|
${this._renderAreas(unassignedAreas.areas)}
|
||||||
|
</wa-tree-item>`
|
||||||
|
: nothing}
|
||||||
|
${this._areaEntries[`area${SEPARATOR}`] &&
|
||||||
|
Object.keys(this._areaEntries[`area${SEPARATOR}`]?.devices).length
|
||||||
|
? html`<wa-tree-item disabled-selection>
|
||||||
|
<ha-svg-icon .path=${mdiDevices}></ha-svg-icon>
|
||||||
|
${this.localize("ui.components.target-picker.type.devices")}
|
||||||
|
${this._renderDevices(
|
||||||
|
this._areaEntries[`area${SEPARATOR}`]?.devices
|
||||||
|
)}
|
||||||
|
</wa-tree-item>`
|
||||||
|
: nothing}
|
||||||
|
${unassignedEntities.length
|
||||||
|
? html`<wa-tree-item disabled-selection>
|
||||||
|
<ha-svg-icon .path=${mdiCubeOutline}></ha-svg-icon>
|
||||||
|
${this.localize("ui.components.target-picker.type.entities")}
|
||||||
|
${this._renderEntities(unassignedEntities)}
|
||||||
|
</wa-tree-item>`
|
||||||
|
: nothing}
|
||||||
|
</wa-tree>`} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderAreas(areas: FloorComboBoxItem[] = []) {
|
||||||
|
if (!areas.length) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.narrow) {
|
||||||
|
return html`<ha-section-title
|
||||||
|
>${this.localize(
|
||||||
|
"ui.components.target-picker.type.areas"
|
||||||
|
)}</ha-section-title
|
||||||
|
>
|
||||||
|
<ha-md-list>
|
||||||
|
${areas.map(({ id, primary, icon, icon_path }) => {
|
||||||
|
if (!this._areaEntries[id]) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${id}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
>
|
||||||
|
${icon
|
||||||
|
? html`<ha-icon slot="start" .icon=${icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${icon_path || mdiTextureBox}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
|
||||||
|
<div slot="headline">${primary}</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>`;
|
||||||
|
})}
|
||||||
|
</ha-md-list>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return areas.map(({ id, primary, icon, icon_path }) => {
|
||||||
|
if (!this._areaEntries[id]) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { open, devices, entities } = this._areaEntries[id];
|
||||||
|
const numberOfDevices = Object.keys(devices).length;
|
||||||
|
const numberOfItems = numberOfDevices + entities.length;
|
||||||
|
|
||||||
|
return html`<wa-tree-item
|
||||||
|
.target=${id}
|
||||||
|
.selected=${this._getSelectedTargetId(this.value) === id}
|
||||||
|
.lazy=${!open && !!numberOfItems}
|
||||||
|
@wa-lazy-load=${this._expandItem}
|
||||||
|
@wa-collapse=${this._collapseItem}
|
||||||
|
>
|
||||||
|
${icon
|
||||||
|
? html`<ha-icon .icon=${icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
.path=${icon_path || mdiTextureBox}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
${primary}
|
||||||
|
${open
|
||||||
|
? html`
|
||||||
|
${numberOfDevices ? this._renderDevices(devices) : nothing}
|
||||||
|
${entities.length ? this._renderEntities(entities) : nothing}
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</wa-tree-item>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderDevices(devices: Record<string, DeviceEntries>) {
|
||||||
|
const renderedDevices = Object.keys(devices).map((deviceId) => {
|
||||||
|
if (!this.devices[deviceId]) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = this.devices[deviceId];
|
||||||
|
const configEntry = device.primary_config_entry
|
||||||
|
? this._configEntryLookup?.[device.primary_config_entry]
|
||||||
|
: undefined;
|
||||||
|
const domain = configEntry?.domain;
|
||||||
|
|
||||||
|
const deviceName = computeDeviceName(device) || deviceId;
|
||||||
|
|
||||||
|
if (this.narrow) {
|
||||||
|
return html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${`device${SEPARATOR}${deviceId}`}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
>
|
||||||
|
${domain
|
||||||
|
? html`
|
||||||
|
<img
|
||||||
|
slot="start"
|
||||||
|
alt=""
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<div slot="headline">${deviceName}</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { open, entities } = devices[deviceId];
|
||||||
|
|
||||||
|
return html`<wa-tree-item
|
||||||
|
.target=${`device${SEPARATOR}${deviceId}`}
|
||||||
|
.selected=${this._getSelectedTargetId(this.value) ===
|
||||||
|
`device${SEPARATOR}${deviceId}`}
|
||||||
|
.lazy=${!open && !!entities.length}
|
||||||
|
@wa-lazy-load=${this._expandItem}
|
||||||
|
@wa-collapse=${this._collapseItem}
|
||||||
|
.title=${deviceName}
|
||||||
|
>
|
||||||
|
${domain
|
||||||
|
? html`
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<span class="item-label">${deviceName}</span>
|
||||||
|
${open ? this._renderEntities(entities) : nothing}
|
||||||
|
</wa-tree-item>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.narrow) {
|
||||||
|
return html`<ha-section-title
|
||||||
|
>${this.localize(
|
||||||
|
"ui.components.target-picker.type.devices"
|
||||||
|
)}</ha-section-title
|
||||||
|
>
|
||||||
|
<ha-md-list> ${renderedDevices} </ha-md-list>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderedDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderEntities(entities: string[] = []) {
|
||||||
|
if (!entities.length) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedEntites = entities.map((entityId) => {
|
||||||
|
const stateObj = this.hass.states[entityId];
|
||||||
|
|
||||||
|
const [entityName, deviceName] = computeEntityNameList(
|
||||||
|
stateObj,
|
||||||
|
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||||
|
this.entities,
|
||||||
|
this.devices,
|
||||||
|
this.areas,
|
||||||
|
this.floors
|
||||||
|
);
|
||||||
|
|
||||||
|
const label = entityName || deviceName || entityId;
|
||||||
|
|
||||||
|
if (this.narrow) {
|
||||||
|
return html`<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.target=${`entity${SEPARATOR}${entityId}`}
|
||||||
|
@click=${this._selectItem}
|
||||||
|
>
|
||||||
|
<state-badge
|
||||||
|
slot="start"
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
.hass=${this.hass}
|
||||||
|
></state-badge>
|
||||||
|
<div slot="headline" class="item-label">${label}</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<wa-tree-item
|
||||||
|
.target=${`entity${SEPARATOR}${entityId}`}
|
||||||
|
.selected=${this._getSelectedTargetId(this.value) ===
|
||||||
|
`entity${SEPARATOR}${entityId}`}
|
||||||
|
.title=${label}
|
||||||
|
>
|
||||||
|
<state-badge .stateObj=${stateObj} .hass=${this.hass}></state-badge>
|
||||||
|
<span class="item-label">${label}</span>
|
||||||
|
</wa-tree-item>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.narrow) {
|
||||||
|
return html`<ha-section-title
|
||||||
|
>${this.localize(
|
||||||
|
"ui.components.target-picker.type.entities"
|
||||||
|
)}</ha-section-title
|
||||||
|
>
|
||||||
|
<ha-md-list>${renderedEntites}</ha-md-list>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderedEntites;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getSelectedTargetId = memoizeOne(
|
||||||
|
(value: SingleHassServiceTarget | undefined) =>
|
||||||
|
value && Object.keys(value).length
|
||||||
|
? `${Object.keys(value)[0].replace("_id", "")}${SEPARATOR}${Object.values(value)[0]}`
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getTreeData() {
|
||||||
|
this._floorAreas = getAreasNestedInFloors(
|
||||||
|
this.states,
|
||||||
|
this.floors,
|
||||||
|
this.areas,
|
||||||
|
this.devices,
|
||||||
|
this.entities,
|
||||||
|
this._formatId,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
this._floorAreas.forEach((floor) => {
|
||||||
|
floor.areas.forEach((area) => {
|
||||||
|
this._loadArea(area);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this._loadUnassignedDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _formatId = memoizeOne((value: AreaFloorValue): string =>
|
||||||
|
[value.type, value.id].join(SEPARATOR)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _handleSelectionChange(ev: WaSelectionChangeEvent) {
|
||||||
|
const treeItem = ev.detail.selection[0] as unknown as
|
||||||
|
| { target?: string }
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (treeItem?.target) {
|
||||||
|
this._valueChanged(treeItem.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _selectItem(ev: CustomEvent) {
|
||||||
|
const target = (ev.currentTarget as any).target;
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
this._valueChanged(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(itemId: string) {
|
||||||
|
const [type, id] = itemId.split(SEPARATOR, 2);
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { [`${type}_id`]: id || undefined },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadUnassignedDevices() {
|
||||||
|
const unassignedDevices = Object.values(this.devices)
|
||||||
|
.filter((device) => !device.area_id)
|
||||||
|
.map(({ id }) => id);
|
||||||
|
|
||||||
|
const devices: Record<string, DeviceEntries> = {};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
unassignedDevices.map(async (deviceId) => {
|
||||||
|
try {
|
||||||
|
const targetEntries = await extractFromTarget(this.hass, {
|
||||||
|
device_id: deviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
devices[deviceId] = {
|
||||||
|
open: false,
|
||||||
|
entities: targetEntries.referenced_entities,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Failed to extract target", e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Object.keys(devices).length) {
|
||||||
|
this._areaEntries = {
|
||||||
|
...this._areaEntries,
|
||||||
|
[`area${SEPARATOR}`]: {
|
||||||
|
open: false,
|
||||||
|
devices,
|
||||||
|
entities: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadArea(area: FloorComboBoxItem) {
|
||||||
|
try {
|
||||||
|
const [, id] = area.id.split(SEPARATOR, 2);
|
||||||
|
const targetEntries = await extractFromTarget(this.hass, {
|
||||||
|
area_id: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const devices: Record<string, DeviceEntries> = {};
|
||||||
|
|
||||||
|
targetEntries.referenced_devices.forEach((device_id) => {
|
||||||
|
devices[device_id] = {
|
||||||
|
open: false,
|
||||||
|
entities: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const entities: string[] = [];
|
||||||
|
|
||||||
|
targetEntries.referenced_entities.forEach((entity_id) => {
|
||||||
|
const entity = this.hass.entities[entity_id];
|
||||||
|
if (entity.device_id && devices[entity.device_id]) {
|
||||||
|
devices[entity.device_id].entities.push(entity_id);
|
||||||
|
} else {
|
||||||
|
entities.push(entity_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._areaEntries = {
|
||||||
|
...this._areaEntries,
|
||||||
|
[area.id]: {
|
||||||
|
open: false,
|
||||||
|
devices,
|
||||||
|
entities,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Failed to extract target", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandItem(ev) {
|
||||||
|
const targetId = ev.target.target;
|
||||||
|
const [type, id] = targetId.split(SEPARATOR, 2);
|
||||||
|
|
||||||
|
if (type === "area") {
|
||||||
|
this._areaEntries = {
|
||||||
|
...this._areaEntries,
|
||||||
|
[targetId]: {
|
||||||
|
...this._areaEntries[targetId],
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (type === "device") {
|
||||||
|
const areaEntry = Object.values(this._areaEntries).find((area) =>
|
||||||
|
Object.keys(area.devices).includes(id)
|
||||||
|
);
|
||||||
|
if (areaEntry) {
|
||||||
|
areaEntry.devices[id].open = true;
|
||||||
|
this._areaEntries = {
|
||||||
|
...this._areaEntries,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _collapseItem(ev) {
|
||||||
|
const targetId = ev.target.target;
|
||||||
|
const [type, id] = targetId.split(SEPARATOR, 2);
|
||||||
|
|
||||||
|
if (type === "area") {
|
||||||
|
this._areaEntries = {
|
||||||
|
...this._areaEntries,
|
||||||
|
[targetId]: {
|
||||||
|
...this._areaEntries[targetId],
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (type === "device") {
|
||||||
|
const areaEntry = Object.values(this._areaEntries).find((area) =>
|
||||||
|
Object.keys(area.devices).includes(id)
|
||||||
|
);
|
||||||
|
if (areaEntry) {
|
||||||
|
areaEntry.devices[id].open = false;
|
||||||
|
this._areaEntries = {
|
||||||
|
...this._areaEntries,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadConfigEntries() {
|
||||||
|
const configEntries = await getConfigEntries(this.hass);
|
||||||
|
this._configEntryLookup = Object.fromEntries(
|
||||||
|
configEntries.map((entry) => [entry.entry_id, entry])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public navigateBack() {
|
||||||
|
if (!this.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueType = Object.keys(this.value)[0].replace("_id", "");
|
||||||
|
const valueId = this.value[`${valueType}_id`];
|
||||||
|
|
||||||
|
if (valueType === "floor" || valueType === "label" || !valueId) {
|
||||||
|
fireEvent(this, "value-changed", { value: undefined });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "area") {
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { floor_id: this.areas[valueId].floor_id },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "device") {
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { area_id: this.devices[valueId].area_id },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueType === "entity") {
|
||||||
|
for (const [areaId, areaEntry] of Object.entries(this._areaEntries)) {
|
||||||
|
const entityDeviceId = this.entities[valueId].device_id;
|
||||||
|
if (entityDeviceId && areaEntry.devices[entityDeviceId]) {
|
||||||
|
// Device is also in area -> go back to device
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areaEntry.entities.includes(valueId)) {
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { area_id: areaId.split(SEPARATOR, 2)[1] },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { device_id: this.entities[valueId].device_id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
--wa-color-neutral-fill-quiet: var(--ha-color-fill-primary-normal-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-section-title {
|
||||||
|
top: 0;
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-tree-item::part(item) {
|
||||||
|
height: var(--ha-space-10);
|
||||||
|
padding: var(--ha-space-1) var(--ha-space-3);
|
||||||
|
cursor: pointer;
|
||||||
|
border-inline-start: 0;
|
||||||
|
}
|
||||||
|
wa-tree-item::part(label) {
|
||||||
|
gap: var(--ha-space-3);
|
||||||
|
font-family: var(--ha-font-family-heading);
|
||||||
|
font-weight: var(--ha-font-weight-medium);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.item-label {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-svg-icon,
|
||||||
|
ha-icon,
|
||||||
|
ha-floor-icon {
|
||||||
|
padding: var(--ha-space-1);
|
||||||
|
color: var(--ha-color-on-neutral-quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-tree-item::part(item):hover {
|
||||||
|
background-color: var(--ha-color-fill-neutral-quiet-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: var(--ha-space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
state-badge {
|
||||||
|
min-width: 32px;
|
||||||
|
max-width: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
max-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-tree-item[selected],
|
||||||
|
wa-tree-item[selected] > ha-svg-icon,
|
||||||
|
wa-tree-item[selected] > ha-icon,
|
||||||
|
wa-tree-item[selected] > ha-floor-icon {
|
||||||
|
color: var(--ha-color-on-primary-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-tree-item[selected]::part(item):hover {
|
||||||
|
background-color: var(--ha-color-fill-primary-normal-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-tree-item::part(base).tree-item-selected .item {
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-md-list {
|
||||||
|
padding: 0;
|
||||||
|
--md-list-item-leading-space: var(--ha-space-3);
|
||||||
|
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||||
|
--md-list-item-bottom-space: var(--ha-space-1);
|
||||||
|
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||||
|
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||||
|
--md-list-item-one-line-container-height: var(--ha-space-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-md-list-item.selected {
|
||||||
|
background-color: var(--ha-color-fill-primary-normal-active);
|
||||||
|
--md-list-item-label-text-color: var(--ha-color-on-primary-normal);
|
||||||
|
--icon-primary-color: var(--ha-color-on-primary-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-md-list-item.selected ha-icon,
|
||||||
|
ha-md-list-item.selected ha-svg-icon {
|
||||||
|
color: var(--ha-color-on-primary-normal);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-automation-add-from-target": HaAutomationAddFromTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state, query } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { mdiClose } from "@mdi/js";
|
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-md-dialog";
|
import "../../../../components/ha-wa-dialog";
|
||||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
import "../../../../components/ha-dialog-footer";
|
||||||
import "../../../../components/ha-dialog-header";
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-form/ha-form";
|
import "../../../../components/ha-form/ha-form";
|
||||||
import "../../../../components/ha-icon-button";
|
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||||
import type { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource";
|
import type { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource";
|
||||||
@@ -43,7 +41,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
|
|
||||||
@state() private _submitting = false;
|
@state() private _submitting = false;
|
||||||
|
|
||||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
@state() private _open = false;
|
||||||
|
|
||||||
public showDialog(params: LovelaceResourceDetailsDialogParams): void {
|
public showDialog(params: LovelaceResourceDetailsDialogParams): void {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
@@ -58,6 +56,11 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
url: "",
|
url: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
this._open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dialogClosed(): void {
|
private _dialogClosed(): void {
|
||||||
@@ -65,10 +68,6 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
|
||||||
this._dialog?.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._params) {
|
if (!this._params) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@@ -81,31 +80,14 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
"ui.panel.config.lovelace.resources.detail.new_resource"
|
"ui.panel.config.lovelace.resources.detail.new_resource"
|
||||||
);
|
);
|
||||||
|
|
||||||
const ariaLabel = this._params.resource?.url
|
|
||||||
? this.hass!.localize(
|
|
||||||
"ui.panel.config.lovelace.resources.detail.edit_resource"
|
|
||||||
)
|
|
||||||
: this.hass!.localize(
|
|
||||||
"ui.panel.config.lovelace.resources.detail.new_resource"
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog
|
<ha-wa-dialog
|
||||||
open
|
.hass=${this.hass}
|
||||||
disable-cancel-action
|
.open=${this._open}
|
||||||
|
prevent-scrim-close
|
||||||
|
header-title=${dialogTitle}
|
||||||
@closed=${this._dialogClosed}
|
@closed=${this._dialogClosed}
|
||||||
.ariaLabel=${ariaLabel}
|
|
||||||
>
|
>
|
||||||
<ha-dialog-header slot="headline">
|
|
||||||
<ha-icon-button
|
|
||||||
slot="navigationIcon"
|
|
||||||
.label=${this.hass.localize("ui.common.close") ?? "Close"}
|
|
||||||
.path=${mdiClose}
|
|
||||||
@click=${this.closeDialog}
|
|
||||||
></ha-icon-button>
|
|
||||||
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
|
|
||||||
</ha-dialog-header>
|
|
||||||
<div slot="content">
|
|
||||||
<ha-alert
|
<ha-alert
|
||||||
alert-type="warning"
|
alert-type="warning"
|
||||||
.title=${this.hass!.localize(
|
.title=${this.hass!.localize(
|
||||||
@@ -118,6 +100,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
</ha-alert>
|
</ha-alert>
|
||||||
|
|
||||||
<ha-form
|
<ha-form
|
||||||
|
autofocus
|
||||||
.schema=${this._schema(this._data)}
|
.schema=${this._schema(this._data)}
|
||||||
.data=${this._data}
|
.data=${this._data}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -125,12 +108,17 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
.computeLabel=${this._computeLabel}
|
.computeLabel=${this._computeLabel}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></ha-form>
|
></ha-form>
|
||||||
</div>
|
|
||||||
<div slot="actions">
|
<ha-dialog-footer slot="footer">
|
||||||
<ha-button appearance="plain" @click=${this.closeDialog}>
|
<ha-button
|
||||||
|
appearance="plain"
|
||||||
|
slot="secondaryAction"
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
>
|
||||||
${this.hass!.localize("ui.common.cancel")}
|
${this.hass!.localize("ui.common.cancel")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
<ha-button
|
<ha-button
|
||||||
|
slot="primaryAction"
|
||||||
@click=${this._updateResource}
|
@click=${this._updateResource}
|
||||||
.disabled=${urlInvalid || !this._data?.res_type || this._submitting}
|
.disabled=${urlInvalid || !this._data?.res_type || this._submitting}
|
||||||
>
|
>
|
||||||
@@ -142,8 +130,8 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
|||||||
"ui.panel.config.lovelace.resources.detail.create"
|
"ui.panel.config.lovelace.resources.detail.create"
|
||||||
)}
|
)}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</div>
|
</ha-dialog-footer>
|
||||||
</ha-md-dialog>
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ function formatTooltip(
|
|||||||
countNegative++;
|
countNegative++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return `${param.marker} ${filterXSS(param.seriesName!)}: ${value} ${unit}`;
|
return `${param.marker} ${filterXSS(param.seriesName!)}: <div style="direction:ltr; display: inline;">${value} ${unit}</div>`;
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
let footer = "";
|
let footer = "";
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export class HuiEnergyDevicesGraphCard
|
|||||||
state: true,
|
state: true,
|
||||||
subscribe: false,
|
subscribe: false,
|
||||||
})
|
})
|
||||||
private _chartType: "bar" | "pie" = "bar";
|
private _chartType?: "bar" | "pie";
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
@@ -101,6 +101,14 @@ export class HuiEnergyDevicesGraphCard
|
|||||||
this._config = config;
|
this._config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getAllowedModes(): ("bar" | "pie")[] {
|
||||||
|
// Empty array or undefined = allow all modes
|
||||||
|
if (!this._config?.modes || this._config.modes.length === 0) {
|
||||||
|
return ["bar", "pie"];
|
||||||
|
}
|
||||||
|
return this._config.modes;
|
||||||
|
}
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
return (
|
return (
|
||||||
hasConfigChanged(this, changedProps) ||
|
hasConfigChanged(this, changedProps) ||
|
||||||
@@ -109,8 +117,21 @@ export class HuiEnergyDevicesGraphCard
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
|
if (changedProps.has("_config") && this._config) {
|
||||||
|
const allowedModes = this._getAllowedModes();
|
||||||
|
|
||||||
|
// If _chartType is not set or not in allowed modes, use first from config
|
||||||
|
if (!this._chartType || !allowedModes.includes(this._chartType)) {
|
||||||
|
this._chartType = allowedModes[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass || !this._config) {
|
if (!this.hass || !this._config || !this._chartType) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,13 +139,19 @@ export class HuiEnergyDevicesGraphCard
|
|||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||||
|
${this._getAllowedModes().length > 1
|
||||||
|
? html`
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.path=${this._chartType === "pie" ? mdiChartBar : mdiChartDonut}
|
.path=${this._chartType === "pie"
|
||||||
|
? mdiChartBar
|
||||||
|
: mdiChartDonut}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
|
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
|
||||||
)}
|
)}
|
||||||
@click=${this._handleChartTypeChange}
|
@click=${this._handleChartTypeChange}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="content ${classMap({
|
class="content ${classMap({
|
||||||
@@ -158,7 +185,7 @@ export class HuiEnergyDevicesGraphCard
|
|||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
|
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
|
||||||
)} kWh`;
|
)} kWh`;
|
||||||
return `${title}${params.marker} ${params.seriesName}: ${value}`;
|
return `${title}${params.marker} ${params.seriesName}: <div style="direction:ltr; display: inline;">${value}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createOptions = memoizeOne(
|
private _createOptions = memoizeOne(
|
||||||
@@ -529,7 +556,13 @@ export class HuiEnergyDevicesGraphCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleChartTypeChange(): void {
|
private _handleChartTypeChange(): void {
|
||||||
this._chartType = this._chartType === "pie" ? "bar" : "pie";
|
if (!this._chartType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allowedModes = this._getAllowedModes();
|
||||||
|
const currentIndex = allowedModes.indexOf(this._chartType);
|
||||||
|
const nextIndex = (currentIndex + 1) % allowedModes.length;
|
||||||
|
this._chartType = allowedModes[nextIndex];
|
||||||
this._getStatistics(this._data!);
|
this._getStatistics(this._data!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -402,7 +402,9 @@ class HuiEnergySankeyCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _valueFormatter = (value: number) =>
|
private _valueFormatter = (value: number) =>
|
||||||
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} kWh`;
|
`<div style="direction:ltr; display: inline;">
|
||||||
|
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
|
||||||
|
kWh</div>`;
|
||||||
|
|
||||||
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||||
const areas: Record<string, { value: number; devices: Node[] }> = {
|
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig {
|
|||||||
title?: string;
|
title?: string;
|
||||||
max_devices?: number;
|
max_devices?: number;
|
||||||
hide_compound_stats?: boolean;
|
hide_compound_stats?: boolean;
|
||||||
|
modes?: ("bar" | "pie")[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnergyDevicesDetailGraphCardConfig
|
export interface EnergyDevicesDetailGraphCardConfig
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export const waColorStyles = css`
|
|||||||
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
|
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
|
||||||
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
|
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
|
||||||
|
|
||||||
|
--wa-color-text-quiet: var(--ha-color-text-secondary);
|
||||||
|
|
||||||
--wa-color-text-normal: var(--ha-color-text-primary);
|
--wa-color-text-normal: var(--ha-color-text-primary);
|
||||||
--wa-color-surface-default: var(--card-background-color);
|
--wa-color-surface-default: var(--card-background-color);
|
||||||
--wa-color-surface-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff));
|
--wa-color-surface-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff));
|
||||||
@@ -62,5 +64,7 @@ export const waColorStyles = css`
|
|||||||
|
|
||||||
--wa-focus-ring-color: var(--ha-color-neutral-60);
|
--wa-focus-ring-color: var(--ha-color-neutral-60);
|
||||||
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
|
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
--wa-color-text-normal: var(--ha-color-text-primary);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ export const waMainStyles = css`
|
|||||||
--wa-focus-ring-offset: 2px;
|
--wa-focus-ring-offset: 2px;
|
||||||
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-focus-ring-color);
|
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-focus-ring-color);
|
||||||
|
|
||||||
|
--wa-space-xs: var(--ha-space-2);
|
||||||
|
--wa-space-m: var(--ha-space-4);
|
||||||
--wa-space-l: var(--ha-space-6);
|
--wa-space-l: var(--ha-space-6);
|
||||||
--wa-space-xl: var(--ha-space-8);
|
--wa-space-xl: var(--ha-space-8);
|
||||||
|
|
||||||
--wa-form-control-padding-block: 0.75em;
|
--wa-form-control-padding-block: 0.75em;
|
||||||
|
--wa-form-control-value-line-height: var(--ha-line-height-condensed);
|
||||||
|
|
||||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||||
|
--wa-transition-normal: 150ms;
|
||||||
--wa-transition-fast: 75ms;
|
--wa-transition-fast: 75ms;
|
||||||
--wa-transition-easing: ease;
|
--wa-transition-easing: ease;
|
||||||
|
|
||||||
@@ -28,6 +32,7 @@ export const waMainStyles = css`
|
|||||||
|
|
||||||
--wa-line-height-condensed: var(--ha-line-height-condensed);
|
--wa-line-height-condensed: var(--ha-line-height-condensed);
|
||||||
|
|
||||||
|
--wa-font-size-m: var(--ha-font-size-m);
|
||||||
--wa-shadow-s: var(--ha-box-shadow-s);
|
--wa-shadow-s: var(--ha-box-shadow-s);
|
||||||
--wa-shadow-m: var(--ha-box-shadow-m);
|
--wa-shadow-m: var(--ha-box-shadow-m);
|
||||||
--wa-shadow-l: var(--ha-box-shadow-l);
|
--wa-shadow-l: var(--ha-box-shadow-l);
|
||||||
|
|||||||
@@ -3990,7 +3990,14 @@
|
|||||||
"item_pasted": "{item} pasted",
|
"item_pasted": "{item} pasted",
|
||||||
"ctrl": "Ctrl",
|
"ctrl": "Ctrl",
|
||||||
"del": "Del",
|
"del": "Del",
|
||||||
|
"targets": "Targets",
|
||||||
|
"select_target": "Select a target",
|
||||||
|
"home": "Home",
|
||||||
|
"unassigned": "Unassigned",
|
||||||
"blocks": "Blocks",
|
"blocks": "Blocks",
|
||||||
|
"show_more": "Show more",
|
||||||
|
"unassigned_entities": "Unassigned entities",
|
||||||
|
"unassigned_devices": "Unassigned devices",
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"name": "Triggers",
|
"name": "Triggers",
|
||||||
"header": "When",
|
"header": "When",
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@@ -9337,7 +9337,7 @@ __metadata:
|
|||||||
husky: "npm:9.1.7"
|
husky: "npm:9.1.7"
|
||||||
idb-keyval: "npm:6.2.2"
|
idb-keyval: "npm:6.2.2"
|
||||||
intl-messageformat: "npm:10.7.18"
|
intl-messageformat: "npm:10.7.18"
|
||||||
js-yaml: "npm:4.1.0"
|
js-yaml: "npm:4.1.1"
|
||||||
jsdom: "npm:27.1.0"
|
jsdom: "npm:27.1.0"
|
||||||
jszip: "npm:3.10.1"
|
jszip: "npm:3.10.1"
|
||||||
leaflet: "npm:1.9.4"
|
leaflet: "npm:1.9.4"
|
||||||
@@ -10407,14 +10407,14 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0":
|
"js-yaml@npm:4.1.1, js-yaml@npm:^4.1.0":
|
||||||
version: 4.1.0
|
version: 4.1.1
|
||||||
resolution: "js-yaml@npm:4.1.0"
|
resolution: "js-yaml@npm:4.1.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse: "npm:^2.0.1"
|
argparse: "npm:^2.0.1"
|
||||||
bin:
|
bin:
|
||||||
js-yaml: bin/js-yaml.js
|
js-yaml: bin/js-yaml.js
|
||||||
checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140
|
checksum: 10/a52d0519f0f4ef5b4adc1cde466cb54c50d56e2b4a983b9d5c9c0f2f99462047007a6274d7e95617a21d3c91fde3ee6115536ed70991cd645ba8521058b78f77
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user