mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-14 21:40:27 +00:00
Compare commits
3 Commits
add-automa
...
renovate/l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c27b903748 | ||
|
|
e0d241a2db | ||
|
|
83e065ae98 |
@@ -153,7 +153,7 @@
|
|||||||
"@babel/plugin-transform-runtime": "7.28.5",
|
"@babel/plugin-transform-runtime": "7.28.5",
|
||||||
"@babel/preset-env": "7.28.5",
|
"@babel/preset-env": "7.28.5",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.21.6",
|
"@bundle-stats/plugin-webpack-filter": "4.21.6",
|
||||||
"@lokalise/node-api": "15.3.1",
|
"@lokalise/node-api": "15.4.0",
|
||||||
"@octokit/auth-oauth-device": "8.0.3",
|
"@octokit/auth-oauth-device": "8.0.3",
|
||||||
"@octokit/plugin-retry": "8.0.3",
|
"@octokit/plugin-retry": "8.0.3",
|
||||||
"@octokit/rest": "22.0.1",
|
"@octokit/rest": "22.0.1",
|
||||||
|
|||||||
@@ -154,10 +154,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this._getLabelsMemoized(
|
return this._getLabelsMemoized(
|
||||||
this.hass.states,
|
this.hass,
|
||||||
this.hass.areas,
|
|
||||||
this.hass.devices,
|
|
||||||
this.hass.entities,
|
|
||||||
this._labels,
|
this._labels,
|
||||||
this.includeDomains,
|
this.includeDomains,
|
||||||
this.excludeDomains,
|
this.excludeDomains,
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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,10 +858,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
if (!filterType || filterType === "label") {
|
if (!filterType || filterType === "label") {
|
||||||
let labels = this._getLabelsMemoized(
|
let labels = this._getLabelsMemoized(
|
||||||
this.hass.states,
|
this.hass,
|
||||||
this.hass.areas,
|
|
||||||
this.hass.devices,
|
|
||||||
this.hass.entities,
|
|
||||||
this._labelRegistry,
|
this._labelRegistry,
|
||||||
includeDomains,
|
includeDomains,
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
@@ -24,54 +24,11 @@ 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"],
|
||||||
@@ -85,47 +42,8 @@ export const getAreasAndFloors = (
|
|||||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
excludeAreas?: string[],
|
excludeAreas?: string[],
|
||||||
excludeFloors?: string[],
|
excludeFloors?: string[]
|
||||||
includeEmptyFloors = false
|
): FloorComboBoxItem[] => {
|
||||||
) =>
|
|
||||||
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);
|
||||||
@@ -271,14 +189,6 @@ const getAreasAndFloorsItems = (
|
|||||||
|
|
||||||
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,
|
||||||
@@ -290,15 +200,9 @@ const getAreasAndFloorsItems = (
|
|||||||
})
|
})
|
||||||
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
|
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
|
||||||
|
|
||||||
const items: (
|
const items: FloorComboBoxItem[] = [];
|
||||||
| 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);
|
||||||
|
|
||||||
@@ -309,7 +213,7 @@ const getAreasAndFloorsItems = (
|
|||||||
})
|
})
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
floorItem = {
|
items.push({
|
||||||
id: formatId({ id: floor.floor_id, type: "floor" }),
|
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||||
type: "floor",
|
type: "floor",
|
||||||
primary: floorName,
|
primary: floorName,
|
||||||
@@ -321,9 +225,25 @@ const getAreasAndFloorsItems = (
|
|||||||
...floor.aliases,
|
...floor.aliases,
|
||||||
...areaSearchLabels,
|
...areaSearchLabels,
|
||||||
],
|
],
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
const floorAreasItems = floorAreas.map((area) => {
|
items.push(
|
||||||
|
...floorAreas.map((area) => {
|
||||||
|
const areaName = computeAreaName(area) || area.area_id;
|
||||||
|
return {
|
||||||
|
id: formatId({ id: area.area_id, type: "area" }),
|
||||||
|
type: "area" as const,
|
||||||
|
primary: areaName,
|
||||||
|
area: area,
|
||||||
|
icon: area.icon || undefined,
|
||||||
|
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
...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" }),
|
||||||
@@ -333,38 +253,8 @@ const getAreasAndFloorsItems = (
|
|||||||
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 (floor) {
|
|
||||||
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;
|
|
||||||
return {
|
|
||||||
id: formatId({ id: area.area_id, type: "area" }),
|
|
||||||
type: "area" as const,
|
|
||||||
primary: areaName,
|
|
||||||
area: area,
|
|
||||||
icon: area.icon || undefined,
|
|
||||||
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,12 +60,11 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getAreaEntityLookup = (
|
export const getAreaEntityLookup = (
|
||||||
entities: EntityRegistryEntry[],
|
entities: EntityRegistryEntry[]
|
||||||
filterHidden = false
|
|
||||||
): AreaEntityLookup => {
|
): AreaEntityLookup => {
|
||||||
const areaEntityLookup: AreaEntityLookup = {};
|
const areaEntityLookup: AreaEntityLookup = {};
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
if (!entity.area_id || (filterHidden && entity.hidden_by)) {
|
if (!entity.area_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!(entity.area_id in areaEntityLookup)) {
|
if (!(entity.area_id in areaEntityLookup)) {
|
||||||
|
|||||||
@@ -107,12 +107,11 @@ export const sortDeviceRegistryByName = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const getDeviceEntityLookup = (
|
export const getDeviceEntityLookup = (
|
||||||
entities: EntityRegistryEntry[],
|
entities: EntityRegistryEntry[]
|
||||||
filterHidden = false
|
|
||||||
): DeviceEntityLookup => {
|
): DeviceEntityLookup => {
|
||||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
if (!entity.device_id || (filterHidden && entity.hidden_by)) {
|
if (!entity.device_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!(entity.device_id in deviceEntityLookup)) {
|
if (!(entity.device_id in deviceEntityLookup)) {
|
||||||
|
|||||||
@@ -360,6 +360,35 @@ export const getReferencedStatisticIds = (
|
|||||||
return statIDs;
|
return statIDs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getReferencedStatisticIdsPower = (
|
||||||
|
prefs: EnergyPreferences
|
||||||
|
): string[] => {
|
||||||
|
const statIDs: (string | undefined)[] = [];
|
||||||
|
|
||||||
|
for (const source of prefs.energy_sources) {
|
||||||
|
if (source.type === "gas" || source.type === "water") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.type === "solar") {
|
||||||
|
statIDs.push(source.stat_rate);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.type === "battery") {
|
||||||
|
statIDs.push(source.stat_rate);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.power) {
|
||||||
|
statIDs.push(...source.power.map((p) => p.stat_rate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statIDs.push(...prefs.device_consumption.map((d) => d.stat_rate));
|
||||||
|
|
||||||
|
return statIDs.filter(Boolean) as string[];
|
||||||
|
};
|
||||||
|
|
||||||
export const enum CompareMode {
|
export const enum CompareMode {
|
||||||
NONE = "",
|
NONE = "",
|
||||||
PREVIOUS = "previous",
|
PREVIOUS = "previous",
|
||||||
@@ -407,9 +436,10 @@ const getEnergyData = async (
|
|||||||
"gas",
|
"gas",
|
||||||
"device",
|
"device",
|
||||||
]);
|
]);
|
||||||
|
const powerStatIds = getReferencedStatisticIdsPower(prefs);
|
||||||
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
|
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
|
||||||
|
|
||||||
const allStatIDs = [...energyStatIds, ...waterStatIds];
|
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
|
||||||
|
|
||||||
const dayDifference = differenceInDays(end || new Date(), start);
|
const dayDifference = differenceInDays(end || new Date(), start);
|
||||||
const period =
|
const period =
|
||||||
@@ -420,6 +450,8 @@ const getEnergyData = async (
|
|||||||
: dayDifference > 2
|
: dayDifference > 2
|
||||||
? "day"
|
? "day"
|
||||||
: "hour";
|
: "hour";
|
||||||
|
const finePeriod =
|
||||||
|
dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
|
||||||
|
|
||||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||||
const statsMetadataArray = allStatIDs.length
|
const statsMetadataArray = allStatIDs.length
|
||||||
@@ -441,6 +473,9 @@ const getEnergyData = async (
|
|||||||
? (gasUnit as (typeof VOLUME_UNITS)[number])
|
? (gasUnit as (typeof VOLUME_UNITS)[number])
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
const powerUnits: StatisticsUnitConfiguration = {
|
||||||
|
power: "kW",
|
||||||
|
};
|
||||||
const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata);
|
const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata);
|
||||||
const waterUnits: StatisticsUnitConfiguration = {
|
const waterUnits: StatisticsUnitConfiguration = {
|
||||||
volume: waterUnit,
|
volume: waterUnit,
|
||||||
@@ -451,6 +486,12 @@ const getEnergyData = async (
|
|||||||
"change",
|
"change",
|
||||||
])
|
])
|
||||||
: {};
|
: {};
|
||||||
|
const _powerStats: Statistics | Promise<Statistics> = powerStatIds.length
|
||||||
|
? fetchStatistics(hass!, start, end, powerStatIds, finePeriod, powerUnits, [
|
||||||
|
"mean",
|
||||||
|
])
|
||||||
|
: {};
|
||||||
|
|
||||||
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
|
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
|
||||||
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
|
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
|
||||||
"change",
|
"change",
|
||||||
@@ -557,6 +598,7 @@ const getEnergyData = async (
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
energyStats,
|
energyStats,
|
||||||
|
powerStats,
|
||||||
waterStats,
|
waterStats,
|
||||||
energyStatsCompare,
|
energyStatsCompare,
|
||||||
waterStatsCompare,
|
waterStatsCompare,
|
||||||
@@ -564,13 +606,14 @@ const getEnergyData = async (
|
|||||||
fossilEnergyConsumptionCompare,
|
fossilEnergyConsumptionCompare,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
_energyStats,
|
_energyStats,
|
||||||
|
_powerStats,
|
||||||
_waterStats,
|
_waterStats,
|
||||||
_energyStatsCompare,
|
_energyStatsCompare,
|
||||||
_waterStatsCompare,
|
_waterStatsCompare,
|
||||||
_fossilEnergyConsumption,
|
_fossilEnergyConsumption,
|
||||||
_fossilEnergyConsumptionCompare,
|
_fossilEnergyConsumptionCompare,
|
||||||
]);
|
]);
|
||||||
const stats = { ...energyStats, ...waterStats };
|
const stats = { ...energyStats, ...waterStats, ...powerStats };
|
||||||
if (compare) {
|
if (compare) {
|
||||||
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,10 +101,7 @@ export const deleteLabelRegistryEntry = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getLabels = (
|
export const getLabels = (
|
||||||
hassStates: HomeAssistant["states"],
|
hass: HomeAssistant,
|
||||||
hassAreas: HomeAssistant["areas"],
|
|
||||||
hassDevices: HomeAssistant["devices"],
|
|
||||||
hassEntities: HomeAssistant["entities"],
|
|
||||||
labels?: LabelRegistryEntry[],
|
labels?: LabelRegistryEntry[],
|
||||||
includeDomains?: string[],
|
includeDomains?: string[],
|
||||||
excludeDomains?: string[],
|
excludeDomains?: string[],
|
||||||
@@ -118,8 +115,8 @@ export const getLabels = (
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const devices = Object.values(hassDevices);
|
const devices = Object.values(hass.devices);
|
||||||
const entities = Object.values(hassEntities);
|
const entities = Object.values(hass.entities);
|
||||||
|
|
||||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||||
@@ -173,7 +170,7 @@ export const getLabels = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return deviceEntityLookup[device.id].some((entity) => {
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
const stateObj = hassStates[entity.entity_id];
|
const stateObj = hass.states[entity.entity_id];
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -184,7 +181,7 @@ export const getLabels = (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
inputEntities = inputEntities!.filter((entity) => {
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
const stateObj = hassStates[entity.entity_id];
|
const stateObj = hass.states[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)
|
||||||
@@ -203,7 +200,7 @@ export const getLabels = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return deviceEntityLookup[device.id].some((entity) => {
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
const stateObj = hassStates[entity.entity_id];
|
const stateObj = hass.states[entity.entity_id];
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -211,7 +208,7 @@ export const getLabels = (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
inputEntities = inputEntities!.filter((entity) => {
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
const stateObj = hassStates[entity.entity_id];
|
const stateObj = hass.states[entity.entity_id];
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -248,7 +245,7 @@ export const getLabels = (
|
|||||||
|
|
||||||
if (areaIds) {
|
if (areaIds) {
|
||||||
areaIds.forEach((areaId) => {
|
areaIds.forEach((areaId) => {
|
||||||
const area = hassAreas[areaId];
|
const area = hass.areas[areaId];
|
||||||
area.labels.forEach((label) => usedLabels.add(label));
|
area.labels.forEach((label) => usedLabels.add(label));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { consume } from "@lit/context";
|
|
||||||
import {
|
import {
|
||||||
mdiAppleKeyboardCommand,
|
mdiAppleKeyboardCommand,
|
||||||
mdiClose,
|
mdiClose,
|
||||||
@@ -6,7 +5,6 @@ import {
|
|||||||
mdiPlus,
|
mdiPlus,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import type { SingleHassServiceTarget } 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 {
|
||||||
@@ -22,17 +20,12 @@ 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";
|
||||||
@@ -46,7 +39,6 @@ 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 "../../../components/ha-wa-dialog";
|
import "../../../components/ha-wa-dialog";
|
||||||
import "../../../components/search-input";
|
import "../../../components/search-input";
|
||||||
@@ -58,10 +50,6 @@ import {
|
|||||||
getService,
|
getService,
|
||||||
isService,
|
isService,
|
||||||
} from "../../../data/action";
|
} from "../../../data/action";
|
||||||
import {
|
|
||||||
getAreaDeviceLookup,
|
|
||||||
getAreaEntityLookup,
|
|
||||||
} from "../../../data/area_registry";
|
|
||||||
import type {
|
import type {
|
||||||
AutomationElementGroup,
|
AutomationElementGroup,
|
||||||
AutomationElementGroupCollection,
|
AutomationElementGroupCollection,
|
||||||
@@ -71,10 +59,6 @@ import {
|
|||||||
CONDITION_COLLECTIONS,
|
CONDITION_COLLECTIONS,
|
||||||
CONDITION_ICONS,
|
CONDITION_ICONS,
|
||||||
} from "../../../data/condition";
|
} from "../../../data/condition";
|
||||||
import { fullEntitiesContext } from "../../../data/context";
|
|
||||||
import { getDeviceEntityLookup } from "../../../data/device_registry";
|
|
||||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
|
||||||
import { getFloorAreaLookup } from "../../../data/floor_registry";
|
|
||||||
import { getServiceIcons } from "../../../data/icons";
|
import { getServiceIcons } from "../../../data/icons";
|
||||||
import type { IntegrationManifest } from "../../../data/integration";
|
import type { IntegrationManifest } from "../../../data/integration";
|
||||||
import {
|
import {
|
||||||
@@ -88,8 +72,6 @@ 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";
|
||||||
|
|
||||||
@@ -144,9 +126,7 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
@state() private _selectedGroup?: string;
|
@state() private _selectedGroup?: string;
|
||||||
|
|
||||||
@state() private _selectedTarget?: SingleHassServiceTarget;
|
@state() private _tab: "groups" | "blocks" = "groups";
|
||||||
|
|
||||||
@state() private _tab: "targets" | "groups" | "blocks" = "targets";
|
|
||||||
|
|
||||||
@state() private _filter = "";
|
@state() private _filter = "";
|
||||||
|
|
||||||
@@ -162,22 +142,12 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
@state() private _narrow = false;
|
@state() private _narrow = false;
|
||||||
|
|
||||||
@state()
|
|
||||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
|
||||||
private fullEntities!: EntityRegistryEntry[];
|
|
||||||
|
|
||||||
@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;
|
||||||
@@ -214,8 +184,7 @@ class DialogAddAutomationElement
|
|||||||
this._bottomSheetMode = false;
|
this._bottomSheetMode = false;
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
this._selectedGroup = undefined;
|
this._selectedGroup = undefined;
|
||||||
this._tab = "targets";
|
this._tab = "groups";
|
||||||
this._selectedTarget = undefined;
|
|
||||||
this._selectedCollectionIndex = undefined;
|
this._selectedCollectionIndex = undefined;
|
||||||
this._filter = "";
|
this._filter = "";
|
||||||
this._manifests = undefined;
|
this._manifests = undefined;
|
||||||
@@ -662,28 +631,27 @@ class DialogAddAutomationElement
|
|||||||
private _renderContent() {
|
private _renderContent() {
|
||||||
const automationElementType = this._params!.type;
|
const automationElementType = this._params!.type;
|
||||||
|
|
||||||
const items =
|
const items = this._filter
|
||||||
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
|
)
|
||||||
)
|
: this._tab === "blocks"
|
||||||
: this._tab === "blocks"
|
? this._getBlockItems(automationElementType, this.hass.localize)
|
||||||
? this._getBlockItems(automationElementType, this.hass.localize)
|
: this._selectedGroup
|
||||||
: this._selectedGroup
|
? this._getGroupItems(
|
||||||
? this._getGroupItems(
|
automationElementType,
|
||||||
automationElementType,
|
this._selectedGroup,
|
||||||
this._selectedGroup,
|
this._selectedCollectionIndex ?? 0,
|
||||||
this._selectedCollectionIndex ?? 0,
|
this._domains,
|
||||||
this._domains,
|
this.hass.localize,
|
||||||
this.hass.localize,
|
this.hass.services,
|
||||||
this.hass.services,
|
this._manifests
|
||||||
this._manifests
|
)
|
||||||
)
|
: undefined;
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const filteredBlockItems =
|
const filteredBlockItems =
|
||||||
this._filter && automationElementType !== "trigger"
|
this._filter && automationElementType !== "trigger"
|
||||||
@@ -703,25 +671,35 @@ class DialogAddAutomationElement
|
|||||||
this._manifests
|
this._manifests
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const groupName = isService(this._selectedGroup)
|
||||||
|
? domainToName(
|
||||||
|
this.hass.localize,
|
||||||
|
getService(this._selectedGroup!),
|
||||||
|
this._manifests?.[getService(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 ||
|
||||||
@@ -730,8 +708,28 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div slot="header">
|
<div slot="header">
|
||||||
${this._renderHeader()}
|
<ha-dialog-header subtitle-position="above">
|
||||||
${!this._narrow || (!this._selectedGroup && !this._selectedTarget)
|
<span slot="title"
|
||||||
|
>${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}
|
||||||
@@ -744,8 +742,9 @@ class DialogAddAutomationElement
|
|||||||
></search-input>
|
></search-input>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
${!this._filter &&
|
${this._params?.type !== "trigger" &&
|
||||||
(!this._narrow || (!this._selectedGroup && !this._selectedTarget))
|
!this._filter &&
|
||||||
|
(!this._narrow || !this._selectedGroup)
|
||||||
? html`<ha-button-toggle-group
|
? html`<ha-button-toggle-group
|
||||||
variant="neutral"
|
variant="neutral"
|
||||||
active-variant="brand"
|
active-variant="brand"
|
||||||
@@ -757,138 +756,110 @@ class DialogAddAutomationElement
|
|||||||
></ha-button-toggle-group>`
|
></ha-button-toggle-group>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="content">
|
||||||
class=${classMap({
|
<ha-md-list
|
||||||
content: true,
|
class=${classMap({
|
||||||
column: this._narrow && this._selectedTarget,
|
groups: true,
|
||||||
})}
|
hidden: hideCollections,
|
||||||
>
|
})}
|
||||||
${this._tab === "targets"
|
>
|
||||||
? html`<ha-automation-add-from-target
|
${this._params!.clipboardItem && !this._filter
|
||||||
.hass=${this.hass}
|
? html`<ha-md-list-item
|
||||||
.value=${this._selectedTarget}
|
interactive
|
||||||
@value-changed=${this._handleTargetSelected}
|
type="button"
|
||||||
.narrow=${this._narrow}
|
class="paste"
|
||||||
class=${this._getAddFromTargetHidden()}
|
.value=${PASTE_VALUE}
|
||||||
></ha-automation-add-from-target>`
|
@click=${this._selected}
|
||||||
: html`
|
>
|
||||||
<ha-md-list
|
<div class="shortcut-label">
|
||||||
class=${classMap({
|
<div class="label">
|
||||||
groups: true,
|
<div>
|
||||||
hidden: hideCollections,
|
${this.hass.localize(
|
||||||
})}
|
`ui.panel.config.automation.editor.${automationElementType}s.paste`
|
||||||
>
|
)}
|
||||||
${this._params!.clipboardItem && !this._filter
|
</div>
|
||||||
? html`<ha-md-list-item
|
<div class="supporting-text">
|
||||||
interactive
|
${this.hass.localize(
|
||||||
type="button"
|
// @ts-ignore
|
||||||
class="paste"
|
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
|
||||||
.value=${PASTE_VALUE}
|
)}
|
||||||
@click=${this._selected}
|
</div>
|
||||||
>
|
</div>
|
||||||
<div class="shortcut-label">
|
${!this._narrow
|
||||||
<div class="label">
|
? html`<span class="shortcut">
|
||||||
<div>
|
<span
|
||||||
${this.hass.localize(
|
>${isMac
|
||||||
`ui.panel.config.automation.editor.${automationElementType}s.paste`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="supporting-text">
|
|
||||||
${this.hass.localize(
|
|
||||||
// @ts-ignore
|
|
||||||
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${!this._narrow
|
|
||||||
? html`<span class="shortcut">
|
|
||||||
<span
|
|
||||||
>${isMac
|
|
||||||
? html`<ha-svg-icon
|
|
||||||
slot="start"
|
|
||||||
.path=${mdiAppleKeyboardCommand}
|
|
||||||
></ha-svg-icon>`
|
|
||||||
: this.hass.localize(
|
|
||||||
"ui.panel.config.automation.editor.ctrl"
|
|
||||||
)}</span
|
|
||||||
>
|
|
||||||
<span>+</span>
|
|
||||||
<span>V</span>
|
|
||||||
</span>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="start"
|
|
||||||
.path=${mdiContentPaste}
|
|
||||||
></ha-svg-icon
|
|
||||||
><ha-svg-icon
|
|
||||||
class="plus"
|
|
||||||
slot="end"
|
|
||||||
.path=${mdiPlus}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</ha-md-list-item>
|
|
||||||
<ha-md-divider
|
|
||||||
role="separator"
|
|
||||||
tabindex="-1"
|
|
||||||
></ha-md-divider>`
|
|
||||||
: nothing}
|
|
||||||
${collections.map(
|
|
||||||
(collection, index) => html`
|
|
||||||
${collection.titleKey
|
|
||||||
? html`<ha-section-title>
|
|
||||||
${this.hass.localize(collection.titleKey)}
|
|
||||||
</ha-section-title>`
|
|
||||||
: nothing}
|
|
||||||
${repeat(
|
|
||||||
collection.groups,
|
|
||||||
(item) => item.key,
|
|
||||||
(item) => html`
|
|
||||||
<ha-md-list-item
|
|
||||||
interactive
|
|
||||||
type="button"
|
|
||||||
.value=${item.key}
|
|
||||||
.index=${index}
|
|
||||||
@click=${this._groupSelected}
|
|
||||||
class=${item.key === this._selectedGroup
|
|
||||||
? "selected"
|
|
||||||
: ""}
|
|
||||||
>
|
|
||||||
<div slot="headline">${item.name}</div>
|
|
||||||
${item.icon
|
|
||||||
? html`<span slot="start">${item.icon}</span>`
|
|
||||||
: item.iconPath
|
|
||||||
? html`<ha-svg-icon
|
? html`<ha-svg-icon
|
||||||
slot="start"
|
slot="start"
|
||||||
.path=${item.iconPath}
|
.path=${mdiAppleKeyboardCommand}
|
||||||
></ha-svg-icon>`
|
></ha-svg-icon>`
|
||||||
: nothing}
|
: this.hass.localize(
|
||||||
</ha-md-list-item>
|
"ui.panel.config.automation.editor.ctrl"
|
||||||
`
|
)}</span
|
||||||
)}
|
>
|
||||||
`
|
<span>+</span>
|
||||||
)}
|
<span>V</span>
|
||||||
</ha-md-list>
|
</span>`
|
||||||
`}
|
: nothing}
|
||||||
|
</div>
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${mdiContentPaste}
|
||||||
|
></ha-svg-icon
|
||||||
|
><ha-svg-icon
|
||||||
|
class="plus"
|
||||||
|
slot="end"
|
||||||
|
.path=${mdiPlus}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-md-list-item>
|
||||||
|
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||||
|
: nothing}
|
||||||
|
${collections.map(
|
||||||
|
(collection, index) => html`
|
||||||
|
${collection.titleKey
|
||||||
|
? html`<div class="collection-title">
|
||||||
|
${this.hass.localize(collection.titleKey)}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
${repeat(
|
||||||
|
collection.groups,
|
||||||
|
(item) => item.key,
|
||||||
|
(item) => html`
|
||||||
|
<ha-md-list-item
|
||||||
|
interactive
|
||||||
|
type="button"
|
||||||
|
.value=${item.key}
|
||||||
|
.index=${index}
|
||||||
|
@click=${this._groupSelected}
|
||||||
|
class=${item.key === this._selectedGroup ? "selected" : ""}
|
||||||
|
>
|
||||||
|
<div slot="headline">${item.name}</div>
|
||||||
|
${item.icon
|
||||||
|
? html`<span slot="start">${item.icon}</span>`
|
||||||
|
: item.iconPath
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${item.iconPath}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: nothing}
|
||||||
|
</ha-md-list-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-md-list>
|
||||||
<div
|
<div
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
items: true,
|
items: true,
|
||||||
blank:
|
blank:
|
||||||
(this._tab === "groups" &&
|
!this._selectedGroup && !this._filter && 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 !== "blocks",
|
this._tab === "groups",
|
||||||
})}
|
})}
|
||||||
@scroll=${this._onItemsScroll}
|
@scroll=${this._onItemsScroll}
|
||||||
>
|
>
|
||||||
@@ -902,27 +873,23 @@ 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
|
: !items?.length &&
|
||||||
? this.hass.localize(
|
this._filter &&
|
||||||
`ui.panel.config.automation.editor.select_target`
|
(!filteredBlockItems || !filteredBlockItems.length)
|
||||||
)
|
? html`<span
|
||||||
: !items?.length &&
|
>${this.hass.localize(
|
||||||
this._filter &&
|
`ui.panel.config.automation.editor.${automationElementType}s.empty_search`,
|
||||||
(!filteredBlockItems || !filteredBlockItems.length)
|
{
|
||||||
? html`<span
|
term: html`<b>‘${this._filter}’</b>`,
|
||||||
>${this.hass.localize(
|
}
|
||||||
`ui.panel.config.automation.editor.${automationElementType}s.empty_search`,
|
)}</span
|
||||||
{
|
>`
|
||||||
term: html`<b>‘${this._filter}’</b>`,
|
: this._renderItemList(
|
||||||
}
|
this.hass.localize(
|
||||||
)}</span
|
`ui.panel.config.automation.editor.${automationElementType}s.name`
|
||||||
>`
|
),
|
||||||
: this._renderItemList(
|
items
|
||||||
this.hass.localize(
|
)}
|
||||||
`ui.panel.config.automation.editor.${automationElementType}s.name`
|
|
||||||
),
|
|
||||||
items
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -1018,10 +985,6 @@ class DialogAddAutomationElement
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _back() {
|
private _back() {
|
||||||
if (this._selectedTarget) {
|
|
||||||
this._targetPickerElement?.navigateBack();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._selectedGroup = undefined;
|
this._selectedGroup = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,21 +1008,6 @@ 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
|
||||||
@@ -1156,183 +1104,6 @@ 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 isService(this._selectedGroup)
|
|
||||||
? domainToName(
|
|
||||||
this.hass.localize,
|
|
||||||
getService(this._selectedGroup!),
|
|
||||||
this._manifests?.[getService(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) {
|
|
||||||
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(getAreaEntityLookup);
|
|
||||||
|
|
||||||
private _getDeviceEntityLookupMemoized = memoizeOne(getDeviceEntityLookup);
|
|
||||||
|
|
||||||
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() {
|
|
||||||
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.fullEntities, true)[targetId]
|
|
||||||
?.length === 0) ||
|
|
||||||
(targetType === "device" &&
|
|
||||||
this._getDeviceEntityLookupMemoized(this.fullEntities, true)[
|
|
||||||
targetId
|
|
||||||
]?.length === 0))
|
|
||||||
) {
|
|
||||||
return "hidden";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
css`
|
css`
|
||||||
@@ -1386,15 +1157,10 @@ 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;
|
||||||
@@ -1402,13 +1168,6 @@ 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);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -1416,8 +1175,7 @@ class DialogAddAutomationElement
|
|||||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||||
--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 {
|
||||||
@@ -1429,9 +1187,16 @@ class DialogAddAutomationElement
|
|||||||
color: var(--ha-color-on-primary-normal);
|
color: var(--ha-color-on-primary-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-section-title {
|
.collection-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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1442,11 +1207,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,810 +0,0 @@
|
|||||||
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 { 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}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
: 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)
|
|
||||||
: 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 _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
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const floor of this._floorAreas) {
|
|
||||||
for (const area of floor.areas) {
|
|
||||||
this._loadArea(area);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 _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") {
|
|
||||||
fireEvent(this, "value-changed", { value: undefined });
|
|
||||||
}
|
|
||||||
|
|
||||||
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 },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,10 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
BarSeriesOption,
|
BarSeriesOption,
|
||||||
CallbackDataParams,
|
CallbackDataParams,
|
||||||
|
LineSeriesOption,
|
||||||
TopLevelFormatterParams,
|
TopLevelFormatterParams,
|
||||||
} from "echarts/types/dist/shared";
|
} from "echarts/types/dist/shared";
|
||||||
|
import type { LineDataItemOption } from "echarts/types/src/chart/line/LineSeries";
|
||||||
import type { FrontendLocaleData } from "../../../../../data/translation";
|
import type { FrontendLocaleData } from "../../../../../data/translation";
|
||||||
import { formatNumber } from "../../../../../common/number/format_number";
|
import { formatNumber } from "../../../../../common/number/format_number";
|
||||||
import {
|
import {
|
||||||
@@ -170,11 +172,10 @@ function formatTooltip(
|
|||||||
compare
|
compare
|
||||||
? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `
|
? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `
|
||||||
: ""
|
: ""
|
||||||
}${formatTime(date, locale, config)} – ${formatTime(
|
}${formatTime(date, locale, config)}`;
|
||||||
addHours(date, 1),
|
if (params[0].componentSubType === "bar") {
|
||||||
locale,
|
period += ` – ${formatTime(addHours(date, 1), locale, config)}`;
|
||||||
config
|
}
|
||||||
)}`;
|
|
||||||
}
|
}
|
||||||
const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`;
|
const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`;
|
||||||
|
|
||||||
@@ -281,6 +282,35 @@ export function fillDataGapsAndRoundCaps(datasets: BarSeriesOption[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fillLineGaps(datasets: LineSeriesOption[]) {
|
||||||
|
const buckets = Array.from(
|
||||||
|
new Set(
|
||||||
|
datasets
|
||||||
|
.map((dataset) =>
|
||||||
|
dataset.data!.map((datapoint) => Number(datapoint![0]))
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
)
|
||||||
|
).sort((a, b) => a - b);
|
||||||
|
buckets.forEach((bucket, index) => {
|
||||||
|
for (let i = datasets.length - 1; i >= 0; i--) {
|
||||||
|
const dataPoint = datasets[i].data![index];
|
||||||
|
const item: LineDataItemOption =
|
||||||
|
dataPoint && typeof dataPoint === "object" && "value" in dataPoint
|
||||||
|
? dataPoint
|
||||||
|
: ({ value: dataPoint } as LineDataItemOption);
|
||||||
|
const x = item.value?.[0];
|
||||||
|
if (x === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Number(x) !== bucket) {
|
||||||
|
datasets[i].data?.splice(index, 0, [bucket, 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return datasets;
|
||||||
|
}
|
||||||
|
|
||||||
export function getCompareTransform(start: Date, compareStart?: Date) {
|
export function getCompareTransform(start: Date, compareStart?: Date) {
|
||||||
if (!compareStart) {
|
if (!compareStart) {
|
||||||
return (ts: Date) => ts;
|
return (ts: Date) => ts;
|
||||||
|
|||||||
335
src/panels/lovelace/cards/energy/hui-power-sources-graph-card.ts
Normal file
335
src/panels/lovelace/cards/energy/hui-power-sources-graph-card.ts
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import { endOfToday, isToday, startOfToday } from "date-fns";
|
||||||
|
import type { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import type { PropertyValues } from "lit";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import type { LineSeriesOption } from "echarts/charts";
|
||||||
|
import { graphic } from "echarts";
|
||||||
|
import "../../../../components/chart/ha-chart-base";
|
||||||
|
import "../../../../components/ha-card";
|
||||||
|
import type { EnergyData } from "../../../../data/energy";
|
||||||
|
import { getEnergyDataCollection } from "../../../../data/energy";
|
||||||
|
import type { StatisticValue } from "../../../../data/recorder";
|
||||||
|
import type { FrontendLocaleData } from "../../../../data/translation";
|
||||||
|
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import type { LovelaceCard } from "../../types";
|
||||||
|
import type { PowerSourcesGraphCardConfig } from "../types";
|
||||||
|
import { hasConfigChanged } from "../../common/has-changed";
|
||||||
|
import { getCommonOptions, fillLineGaps } from "./common/energy-chart-options";
|
||||||
|
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||||
|
import { hex2rgb } from "../../../../common/color/convert-color";
|
||||||
|
|
||||||
|
@customElement("hui-power-sources-graph-card")
|
||||||
|
export class HuiPowerSourcesGraphCard
|
||||||
|
extends SubscribeMixin(LitElement)
|
||||||
|
implements LovelaceCard
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _config?: PowerSourcesGraphCardConfig;
|
||||||
|
|
||||||
|
@state() private _chartData: LineSeriesOption[] = [];
|
||||||
|
|
||||||
|
@state() private _start = startOfToday();
|
||||||
|
|
||||||
|
@state() private _end = endOfToday();
|
||||||
|
|
||||||
|
@state() private _compareStart?: Date;
|
||||||
|
|
||||||
|
@state() private _compareEnd?: Date;
|
||||||
|
|
||||||
|
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||||
|
|
||||||
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
|
return [
|
||||||
|
getEnergyDataCollection(this.hass, {
|
||||||
|
key: this._config?.collection_key,
|
||||||
|
}).subscribe((data) => this._getStatistics(data)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCardSize(): Promise<number> | number {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config: PowerSourcesGraphCardConfig): void {
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
|
return (
|
||||||
|
hasConfigChanged(this, changedProps) ||
|
||||||
|
changedProps.size > 1 ||
|
||||||
|
!changedProps.has("hass")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass || !this._config) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-card>
|
||||||
|
${this._config.title
|
||||||
|
? html`<h1 class="card-header">${this._config.title}</h1>`
|
||||||
|
: ""}
|
||||||
|
<div
|
||||||
|
class="content ${classMap({
|
||||||
|
"has-header": !!this._config.title,
|
||||||
|
})}"
|
||||||
|
>
|
||||||
|
<ha-chart-base
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${this._chartData}
|
||||||
|
.options=${this._createOptions(
|
||||||
|
this._start,
|
||||||
|
this._end,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this._compareStart,
|
||||||
|
this._compareEnd
|
||||||
|
)}
|
||||||
|
></ha-chart-base>
|
||||||
|
${!this._chartData.some((dataset) => dataset.data!.length)
|
||||||
|
? html`<div class="no-data">
|
||||||
|
${isToday(this._start)
|
||||||
|
? this.hass.localize("ui.panel.lovelace.cards.energy.no_data")
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.cards.energy.no_data_period"
|
||||||
|
)}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createOptions = memoizeOne(
|
||||||
|
(
|
||||||
|
start: Date,
|
||||||
|
end: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig,
|
||||||
|
compareStart?: Date,
|
||||||
|
compareEnd?: Date
|
||||||
|
): ECOption =>
|
||||||
|
getCommonOptions(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
locale,
|
||||||
|
config,
|
||||||
|
"kW",
|
||||||
|
compareStart,
|
||||||
|
compareEnd
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
||||||
|
const datasets: LineSeriesOption[] = [];
|
||||||
|
|
||||||
|
const statIds = {
|
||||||
|
solar: {
|
||||||
|
stats: [] as string[],
|
||||||
|
color: "--energy-solar-color",
|
||||||
|
name: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.cards.energy.power_graph.solar"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
stats: [] as string[],
|
||||||
|
color: "--energy-grid-consumption-color",
|
||||||
|
name: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.cards.energy.power_graph.grid"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
battery: {
|
||||||
|
stats: [] as string[],
|
||||||
|
color: "--energy-battery-out-color",
|
||||||
|
name: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.cards.energy.power_graph.battery"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
|
||||||
|
for (const source of energyData.prefs.energy_sources) {
|
||||||
|
if (source.type === "solar") {
|
||||||
|
if (source.stat_rate) {
|
||||||
|
statIds.solar.stats.push(source.stat_rate);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.type === "battery") {
|
||||||
|
if (source.stat_rate) {
|
||||||
|
statIds.battery.stats.push(source.stat_rate);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.type === "grid" && source.power) {
|
||||||
|
statIds.grid.stats.push(...source.power.map((p) => p.stat_rate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const commonSeriesOptions: LineSeriesOption = {
|
||||||
|
type: "line",
|
||||||
|
smooth: 0.4,
|
||||||
|
smoothMonotone: "x",
|
||||||
|
lineStyle: {
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(statIds).forEach((key, keyIndex) => {
|
||||||
|
if (statIds[key].stats.length) {
|
||||||
|
const colorHex = computedStyles.getPropertyValue(statIds[key].color);
|
||||||
|
const rgb = hex2rgb(colorHex);
|
||||||
|
// Echarts is supposed to handle that but it is bugged when you use it together with stacking.
|
||||||
|
// The interpolation breaks the stacking, so this positive/negative is a workaround
|
||||||
|
const { positive, negative } = this._processData(
|
||||||
|
statIds[key].stats.map((id: string) => energyData.stats[id] ?? [])
|
||||||
|
);
|
||||||
|
datasets.push({
|
||||||
|
...commonSeriesOptions,
|
||||||
|
id: key,
|
||||||
|
name: statIds[key].name,
|
||||||
|
color: colorHex,
|
||||||
|
stack: "positive",
|
||||||
|
areaStyle: {
|
||||||
|
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
data: positive,
|
||||||
|
z: 3 - keyIndex, // draw in reverse order so 0 value lines are overwritten
|
||||||
|
});
|
||||||
|
if (key !== "solar") {
|
||||||
|
datasets.push({
|
||||||
|
...commonSeriesOptions,
|
||||||
|
id: `${key}-negative`,
|
||||||
|
name: statIds[key].name,
|
||||||
|
color: colorHex,
|
||||||
|
stack: "negative",
|
||||||
|
areaStyle: {
|
||||||
|
color: new graphic.LinearGradient(0, 1, 0, 0, [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
data: negative,
|
||||||
|
z: 4 - keyIndex, // draw in reverse order but above positive series
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._start = energyData.start;
|
||||||
|
this._end = energyData.end || endOfToday();
|
||||||
|
|
||||||
|
this._chartData = fillLineGaps(datasets);
|
||||||
|
|
||||||
|
const usageData: NonNullable<LineSeriesOption["data"]> = [];
|
||||||
|
this._chartData[0]?.data!.forEach((item, i) => {
|
||||||
|
// fillLineGaps ensures all datasets have the same x values
|
||||||
|
const x =
|
||||||
|
typeof item === "object" && "value" in item!
|
||||||
|
? item.value![0]
|
||||||
|
: item![0];
|
||||||
|
usageData[i] = [x, 0];
|
||||||
|
this._chartData.forEach((dataset) => {
|
||||||
|
const y =
|
||||||
|
typeof dataset.data![i] === "object" && "value" in dataset.data![i]!
|
||||||
|
? dataset.data![i].value![1]
|
||||||
|
: dataset.data![i]![1];
|
||||||
|
usageData[i]![1] += y as number;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._chartData.push({
|
||||||
|
...commonSeriesOptions,
|
||||||
|
id: "usage",
|
||||||
|
name: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.cards.energy.power_graph.usage"
|
||||||
|
),
|
||||||
|
color: computedStyles.getPropertyValue("--primary-color"),
|
||||||
|
lineStyle: { width: 2 },
|
||||||
|
data: usageData,
|
||||||
|
z: 5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _processData(stats: StatisticValue[][]) {
|
||||||
|
const data: Record<number, number[]> = {};
|
||||||
|
stats.forEach((statSet) => {
|
||||||
|
statSet.forEach((point) => {
|
||||||
|
if (point.mean == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const x = (point.start + point.end) / 2;
|
||||||
|
data[x] = [...(data[x] ?? []), point.mean];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const positive: [number, number][] = [];
|
||||||
|
const negative: [number, number][] = [];
|
||||||
|
Object.entries(data).forEach(([x, y]) => {
|
||||||
|
const ts = Number(x);
|
||||||
|
const meanY = y.reduce((a, b) => a + b, 0) / y.length;
|
||||||
|
positive.push([ts, Math.max(0, meanY)]);
|
||||||
|
negative.push([ts, Math.min(0, meanY)]);
|
||||||
|
});
|
||||||
|
return { positive, negative };
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
ha-card {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: var(--ha-space-4);
|
||||||
|
}
|
||||||
|
.has-header {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
.no-data {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20%;
|
||||||
|
margin-left: var(--ha-space-8);
|
||||||
|
margin-inline-start: var(--ha-space-8);
|
||||||
|
margin-inline-end: initial;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-power-sources-graph-card": HuiPowerSourcesGraphCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -230,6 +230,11 @@ export interface EnergySankeyCardConfig extends EnergyCardBaseConfig {
|
|||||||
group_by_area?: boolean;
|
group_by_area?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
|
||||||
|
type: "power-sources-graph";
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
||||||
type: "entity-filter";
|
type: "entity-filter";
|
||||||
entities: (EntityFilterEntityConfig | string)[];
|
entities: (EntityFilterEntityConfig | string)[];
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ const LAZY_LOAD_TYPES = {
|
|||||||
"energy-usage-graph": () =>
|
"energy-usage-graph": () =>
|
||||||
import("../cards/energy/hui-energy-usage-graph-card"),
|
import("../cards/energy/hui-energy-usage-graph-card"),
|
||||||
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
||||||
|
"power-sources-graph": () =>
|
||||||
|
import("../cards/energy/hui-power-sources-graph-card"),
|
||||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||||
error: () => import("../cards/hui-error-card"),
|
error: () => import("../cards/hui-error-card"),
|
||||||
"home-summary": () => import("../cards/hui-home-summary-card"),
|
"home-summary": () => import("../cards/hui-home-summary-card"),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const NON_STANDARD_URLS = {
|
|||||||
"energy-devices-graph": "energy/#devices-energy-graph",
|
"energy-devices-graph": "energy/#devices-energy-graph",
|
||||||
"energy-devices-detail-graph": "energy/#detail-devices-energy-graph",
|
"energy-devices-detail-graph": "energy/#detail-devices-energy-graph",
|
||||||
"energy-sankey": "energy/#sankey-energy-graph",
|
"energy-sankey": "energy/#sankey-energy-graph",
|
||||||
|
"power-sources-graph": "energy/#power-sources-graph",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCardDocumentationURL = (
|
export const getCardDocumentationURL = (
|
||||||
|
|||||||
@@ -199,23 +199,3 @@ export const baseEntrypointStyles = css`
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const baseAnimationStyles = css`
|
|
||||||
@keyframes fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-out {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|||||||
30
src/resources/theme/animations.globals.ts
Normal file
30
src/resources/theme/animations.globals.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { css } from "lit";
|
||||||
|
|
||||||
|
export const animationStyles = css`
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale {
|
||||||
|
from {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -52,8 +52,6 @@ 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));
|
||||||
@@ -64,7 +62,5 @@ 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);
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fontStyles } from "../roboto";
|
import { fontStyles } from "../roboto";
|
||||||
|
import { animationStyles } from "./animations.globals";
|
||||||
import { colorDerivedVariables, colorStylesCollection } from "./color";
|
import { colorDerivedVariables, colorStylesCollection } from "./color";
|
||||||
import { coreDerivedVariables, coreStyles } from "./core.globals";
|
import { coreDerivedVariables, coreStyles } from "./core.globals";
|
||||||
import { mainDerivedVariables, mainStyles } from "./main.globals";
|
import { mainDerivedVariables, mainStyles } from "./main.globals";
|
||||||
@@ -17,6 +18,7 @@ export const themeStyles = [
|
|||||||
...colorStylesCollection,
|
...colorStylesCollection,
|
||||||
fontStyles.toString(),
|
fontStyles.toString(),
|
||||||
waMainStyles.toString(),
|
waMainStyles.toString(),
|
||||||
|
animationStyles.toString(),
|
||||||
].join("");
|
].join("");
|
||||||
|
|
||||||
export const derivedStyles = {
|
export const derivedStyles = {
|
||||||
|
|||||||
@@ -9,16 +9,12 @@ 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;
|
||||||
|
|
||||||
@@ -32,7 +28,6 @@ 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);
|
||||||
|
|||||||
@@ -3989,9 +3989,6 @@
|
|||||||
"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",
|
|
||||||
"blocks": "Blocks",
|
"blocks": "Blocks",
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"name": "Triggers",
|
"name": "Triggers",
|
||||||
@@ -7164,6 +7161,12 @@
|
|||||||
"low_carbon_energy_consumed": "Low-carbon electricity consumed",
|
"low_carbon_energy_consumed": "Low-carbon electricity consumed",
|
||||||
"low_carbon_energy_not_calculated": "Consumed low-carbon electricity couldn't be calculated"
|
"low_carbon_energy_not_calculated": "Consumed low-carbon electricity couldn't be calculated"
|
||||||
},
|
},
|
||||||
|
"power_graph": {
|
||||||
|
"grid": "Grid",
|
||||||
|
"solar": "Solar",
|
||||||
|
"battery": "Battery",
|
||||||
|
"usage": "Used"
|
||||||
|
},
|
||||||
"energy_compare": {
|
"energy_compare": {
|
||||||
"info": "You are comparing the period {start} with the period {end}",
|
"info": "You are comparing the period {start} with the period {end}",
|
||||||
"compare_previous_year": "Compare previous year",
|
"compare_previous_year": "Compare previous year",
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@@ -2365,10 +2365,10 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lokalise/node-api@npm:15.3.1":
|
"@lokalise/node-api@npm:15.4.0":
|
||||||
version: 15.3.1
|
version: 15.4.0
|
||||||
resolution: "@lokalise/node-api@npm:15.3.1"
|
resolution: "@lokalise/node-api@npm:15.4.0"
|
||||||
checksum: 10/9175559660cfbde3f6451ee0ade96ca5ccf6686f3a8f07a23ae6abf3a58db5b5dc71683cdb7f19252765250df7b77dc67539a80e24c3b44a1a97bb2f2d9cd090
|
checksum: 10/fe7e36bb137310244079fba9978a10fdf65ca6566e075e8e25ed0fd461e7168649ca43929a0a3a0eaf2df72055996ef4d8a72302e7b502863feb9f9a6471aff1
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -9233,7 +9233,7 @@ __metadata:
|
|||||||
"@lit-labs/virtualizer": "npm:2.1.1"
|
"@lit-labs/virtualizer": "npm:2.1.1"
|
||||||
"@lit/context": "npm:1.1.6"
|
"@lit/context": "npm:1.1.6"
|
||||||
"@lit/reactive-element": "npm:2.1.1"
|
"@lit/reactive-element": "npm:2.1.1"
|
||||||
"@lokalise/node-api": "npm:15.3.1"
|
"@lokalise/node-api": "npm:15.4.0"
|
||||||
"@material/chips": "npm:=14.0.0-canary.53b3cad2f.0"
|
"@material/chips": "npm:=14.0.0-canary.53b3cad2f.0"
|
||||||
"@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0"
|
"@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0"
|
||||||
"@material/mwc-base": "npm:0.27.0"
|
"@material/mwc-base": "npm:0.27.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user