Compare commits

..

3 Commits

Author SHA1 Message Date
Paul Bottein
dd1d63734a Rename 2025-11-25 19:00:06 +01:00
Paul Bottein
6dac37c535 Fix panel energy wizard 2025-11-25 18:58:42 +01:00
Paul Bottein
14e1bdb586 Rename home dashboard path and strategy name (#28104) 2025-11-25 17:03:58 +01:00
26 changed files with 911 additions and 4569 deletions

View File

@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-ha.0",
"@home-assistant/webawesome": "3.0.0",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",

View File

@@ -6,7 +6,7 @@ import {
mdiHomeFloor3,
mdiHomeFloorNegative1,
} from "@mdi/js";
import { LitElement, html, nothing } from "lit";
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import type { FloorRegistryEntry } from "../data/floor_registry";
import "./ha-icon";
@@ -48,7 +48,7 @@ export const floorDefaultIcon = (floor: Pick<FloorRegistryEntry, "level">) => {
@customElement("ha-floor-icon")
export class HaFloorIcon extends LitElement {
@property({ attribute: false }) public floor?: Pick<
@property({ attribute: false }) public floor!: Pick<
FloorRegistryEntry,
"icon" | "level"
>;
@@ -56,9 +56,6 @@ export class HaFloorIcon extends LitElement {
@property() public icon?: string;
protected render() {
if (!this.floor) {
return nothing;
}
if (this.floor.icon) {
return html`<ha-icon .icon=${this.floor.icon}></ha-icon>`;
}

View File

@@ -154,10 +154,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
}
return this._getLabelsMemoized(
this.hass.states,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this.hass,
this._labels,
this.includeDomains,
this.excludeDomains,

View File

@@ -192,7 +192,7 @@ export class HaPickerComboBox extends LitElement {
@focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
>
</lit-virtualizer>`;
</lit-virtualizer> `;
}
private _renderSectionButtons() {

View File

@@ -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 {
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;
box-sizing: border-box;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-section-title": HaSectionTitle;
}
}

View File

@@ -30,7 +30,6 @@ import {
areaMeetsFilter,
deviceMeetsFilter,
entityRegMeetsFilter,
getTargetComboBoxItemType,
type TargetType,
type TargetTypeFloorless,
} from "../data/target";
@@ -48,6 +47,7 @@ import "./ha-tree-indicator";
import "./target-picker/ha-target-picker-item-group";
import "./target-picker/ha-target-picker-value-chip";
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
const SEPARATOR = "________";
const CREATE_ID = "___create-new-entity___";
@@ -634,6 +634,35 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
private _getRowType = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem
) => {
if (
(item as FloorComboBoxItem).type === "area" ||
(item as FloorComboBoxItem).type === "floor"
) {
return (item as FloorComboBoxItem).type;
}
if ("domain" in item) {
return "device";
}
if ("stateObj" in item) {
return "entity";
}
if (item.id === EMPTY_SEARCH) {
return "empty";
}
return "label";
};
private _sectionTitleFunction = ({
firstIndex,
lastIndex,
@@ -657,7 +686,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
const type = getTargetComboBoxItemType(firstItem as PickerComboBoxItem);
const type = this._getRowType(firstItem as PickerComboBoxItem);
const translationType:
| "areas"
| "entities"
@@ -829,10 +858,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (!filterType || filterType === "label") {
let labels = this._getLabelsMemoized(
this.hass.states,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this.hass,
this._labelRegistry,
includeDomains,
undefined,
@@ -948,7 +974,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return nothing;
}
const type = getTargetComboBoxItemType(item);
const type = this._getRowType(item);
let hasFloor = false;
let rtl = false;
let showEntityId = false;

View File

@@ -235,7 +235,7 @@ export class HaWaDialog extends LitElement {
}
:host([width="large"]) wa-dialog {
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
--width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
}
:host([width="full"]) wa-dialog {

View File

@@ -21,52 +21,11 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
area?: AreaRegistryEntry;
}
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
floor?: FloorRegistryEntry;
areas: FloorComboBoxItem[];
}
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
areas: FloorComboBoxItem[];
}
export interface AreaFloorValue {
id: string;
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[]
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors,
true
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
export const getAreasAndFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
@@ -81,43 +40,7 @@ export const getAreasAndFloors = (
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[]
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors
) 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[],
nested = false
): (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] => {
): FloorComboBoxItem[] => {
const floors = Object.values(haFloors);
const areas = Object.values(haAreas);
const devices = Object.values(haDevices);
@@ -258,11 +181,7 @@ const getAreasAndFloorsItems = (
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
const items: (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] = [];
const items: FloorComboBoxItem[] = [];
hierarchy.floors.forEach((f) => {
const floor = haFloors[f.id];
@@ -277,7 +196,7 @@ const getAreasAndFloorsItems = (
})
.flat();
const floorItem: FloorComboBoxItem | FloorNestedComboBoxItem = {
items.push({
id: formatId({ id: floor.floor_id, type: "floor" }),
type: "floor",
primary: floorName,
@@ -289,53 +208,41 @@ const getAreasAndFloorsItems = (
...floor.aliases,
...areaSearchLabels,
],
};
});
items.push(floorItem);
items.push(
...floorAreas.map((area) => {
const areaName = computeAreaName(area);
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName || area.area_id,
area: area,
icon: area.icon || undefined,
search_labels: [
area.area_id,
...(areaName ? [areaName] : []),
...area.aliases,
],
};
})
);
});
const floorAreasItems = floorAreas.map((area) => {
const areaName = computeAreaName(area);
items.push(
...hierarchy.areas.map((areaId) => {
const area = haAreas[areaId];
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName || area.area_id,
primary: areaName,
area: area,
icon: area.icon || undefined,
search_labels: [
area.area_id,
...(areaName ? [areaName] : []),
...area.aliases,
],
search_labels: [area.area_id, areaName, ...area.aliases],
};
});
if (nested && floor) {
(floorItem as unknown as FloorNestedComboBoxItem).areas = floorAreasItems;
} else {
items.push(...floorAreasItems);
}
});
const unassignedAreaItems = hierarchy.areas.map((areaId) => {
const area = haAreas[areaId];
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;
};

View File

@@ -1,10 +1,7 @@
import { stringCompare } from "../common/string/compare";
import type { HomeAssistant } from "../types";
import type { DeviceRegistryEntry } from "./device_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "./entity_registry";
import type { EntityRegistryEntry } from "./entity_registry";
import type { RegistryEntry } from "./registry";
export { subscribeAreaRegistry } from "./ws-area_registry";
@@ -21,10 +18,7 @@ export interface AreaRegistryEntry extends RegistryEntry {
temperature_entity_id: string | null;
}
export type AreaEntityLookup = Record<
string,
(EntityRegistryEntry | EntityRegistryDisplayEntry)[]
>;
export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
@@ -75,17 +69,11 @@ export const reorderAreaRegistryEntries = (
});
export const getAreaEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
filterHidden = false
entities: EntityRegistryEntry[]
): AreaEntityLookup => {
const areaEntityLookup: AreaEntityLookup = {};
for (const entity of entities) {
if (
!entity.area_id ||
(filterHidden &&
((entity as EntityRegistryDisplayEntry).hidden ||
(entity as EntityRegistryEntry).hidden_by))
) {
if (!entity.area_id) {
continue;
}
if (!(entity.area_id in areaEntityLookup)) {

View File

@@ -50,11 +50,7 @@ export type DeviceEntityDisplayLookup = Record<
EntityRegistryDisplayEntry[]
>;
export type DeviceEntityLookup<
T extends EntityRegistryEntry | EntityRegistryDisplayEntry =
| EntityRegistryEntry
| EntityRegistryDisplayEntry,
> = Record<string, T[]>;
export type DeviceEntityLookup = Record<string, EntityRegistryEntry[]>;
export interface DeviceRegistryEntryMutableParams {
area_id?: string | null;
@@ -111,17 +107,11 @@ export const sortDeviceRegistryByName = (
);
export const getDeviceEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
filterHidden = false
entities: EntityRegistryEntry[]
): DeviceEntityLookup => {
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (
!entity.device_id ||
(filterHidden &&
((entity as EntityRegistryDisplayEntry).hidden ||
(entity as EntityRegistryEntry).hidden_by))
) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {

View File

@@ -1,8 +1,8 @@
import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
import type { LocalizeFunc } from "../common/translations/localize";
import { debounce } from "../common/util/debounce";
import type { HomeAssistant } from "../types";
import { debounce } from "../common/util/debounce";
export const integrationsWithPanel = {
bluetooth: "config/bluetooth",
@@ -25,8 +25,6 @@ export type IntegrationType =
| "entity"
| "system";
export type DomainManifestLookup = Record<string, IntegrationManifest>;
export interface IntegrationManifest {
is_built_in: boolean;
overwrites_built_in?: boolean;

View File

@@ -101,10 +101,7 @@ export const deleteLabelRegistryEntry = (
});
export const getLabels = (
hassStates: HomeAssistant["states"],
hassAreas: HomeAssistant["areas"],
hassDevices: HomeAssistant["devices"],
hassEntities: HomeAssistant["entities"],
hass: HomeAssistant,
labels?: LabelRegistryEntry[],
includeDomains?: string[],
excludeDomains?: string[],
@@ -118,8 +115,8 @@ export const getLabels = (
return [];
}
const devices = Object.values(hassDevices);
const entities = Object.values(hassEntities);
const devices = Object.values(hass.devices);
const entities = Object.values(hass.entities);
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
@@ -173,7 +170,7 @@ export const getLabels = (
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -184,9 +181,8 @@ export const getLabels = (
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
return (
stateObj &&
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
@@ -204,7 +200,7 @@ export const getLabels = (
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -212,7 +208,7 @@ export const getLabels = (
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -249,8 +245,8 @@ export const getLabels = (
if (areaIds) {
areaIds.forEach((areaId) => {
const area = hassAreas[areaId];
area?.labels.forEach((label) => usedLabels.add(label));
const area = hass.areas[areaId];
area.labels.forEach((label) => usedLabels.add(label));
});
}

View File

@@ -1,30 +1,15 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { HomeAssistant } from "../types";
import type { FloorComboBoxItem } from "./area_floor";
import type { AreaRegistryEntry } from "./area_registry";
import type { DevicePickerItem, DeviceRegistryEntry } from "./device_registry";
import type { DeviceRegistryEntry } from "./device_registry";
import type { HaEntityPickerEntityFilterFunc } from "./entity";
import type {
EntityComboBoxItem,
EntityRegistryDisplayEntry,
} from "./entity_registry";
export const TARGET_SEPARATOR = "________";
import type { EntityRegistryDisplayEntry } from "./entity_registry";
export type TargetType = "entity" | "device" | "area" | "label" | "floor";
export type TargetTypeFloorless = Exclude<TargetType, "floor">;
export interface SingleHassServiceTarget {
entity_id?: string;
device_id?: string;
area_id?: string;
floor_id?: string;
label_id?: string;
}
export interface ExtractFromTargetResult {
missing_areas: string[];
missing_devices: string[];
@@ -50,28 +35,6 @@ export const extractFromTarget = async (
target,
});
export const getTriggersForTarget = async (
callWS: HomeAssistant["callWS"],
target: HassServiceTarget,
expandGroup = true
) =>
callWS<string[]>({
type: "get_triggers_for_target",
target,
expand_group: expandGroup,
});
export const getServicesForTarget = async (
callWS: HomeAssistant["callWS"],
target: HassServiceTarget,
expandGroup = true
) =>
callWS<string[]>({
type: "get_services_for_target",
target,
expand_group: expandGroup,
});
export const areaMeetsFilter = (
area: AreaRegistryEntry,
devices: HomeAssistant["devices"],
@@ -199,32 +162,3 @@ export const entityRegMeetsFilter = (
}
return true;
};
export const getTargetComboBoxItemType = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem
) => {
if (
(item as FloorComboBoxItem).type === "area" ||
(item as FloorComboBoxItem).type === "floor"
) {
return (item as FloorComboBoxItem).type;
}
if ("domain" in item) {
return "device";
}
if ("stateObj" in item) {
return "entity";
}
if (item.id === "___EMPTY_SEARCH___") {
return "empty";
}
return "label";
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,384 +0,0 @@
import {
mdiInformationOutline,
mdiLabel,
mdiPlus,
mdiTextureBox,
} from "@mdi/js";
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-domain-icon";
import "../../../../components/ha-floor-icon";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import type { ConfigEntry } from "../../../../data/config_entries";
import type { HomeAssistant } from "../../../../types";
import type { AddAutomationElementListItem } from "../add-automation-element-dialog";
type Target = [string, string | undefined, string | undefined];
@customElement("ha-automation-add-items")
export class HaAutomationAddItems extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public items?: {
title: string;
items: AddAutomationElementListItem[];
}[];
@property() public error?: string;
@property({ attribute: "select-label" }) public selectLabel!: string;
@property({ attribute: "empty-label" }) public emptyLabel!: string;
@property({ attribute: false }) public target?: Target;
@property({ attribute: false }) public getLabel!: (
id: string
) => { name: string; icon?: string } | undefined;
@property({ attribute: false }) public configEntryLookup: Record<
string,
ConfigEntry
> = {};
@property({ type: Boolean, attribute: "tooltip-description" })
public tooltipDescription = false;
@state() private _itemsScrolled = false;
@query(".items")
private _itemsDiv!: HTMLDivElement;
protected render() {
return html`<div
class=${classMap({
items: true,
blank: this.error || !this.items || !this.items.length,
error: this.error,
scrolled: this._itemsScrolled,
})}
@scroll=${this._onItemsScroll}
>
${!this.items && !this.error
? this.selectLabel
: this.error
? html`${this.error}
<div>${this._renderTarget(this.target)}</div>`
: this.items && !this.items.length
? html`${this.emptyLabel}
${this.target
? html`<div>${this._renderTarget(this.target)}</div>`
: nothing}`
: repeat(
this.items,
(_, index) => `item-group-${index}`,
(itemGroup) =>
this._renderItemList(itemGroup.title, itemGroup.items)
)}
</div>`;
}
private _renderItemList(title, items?: AddAutomationElementListItem[]) {
if (!items || !items.length) {
return nothing;
}
return html`
<div class="items-title">${title}</div>
<ha-md-list>
${repeat(
items,
(item) => item.key,
(item) => html`
<ha-md-list-item
interactive
type="button"
.value=${item.key}
@click=${this._selected}
>
<div slot="headline" class=${this.target ? "item-headline" : ""}>
${item.name}${this._renderTarget(this.target)}
</div>
${!this.tooltipDescription && item.description
? html`<div slot="supporting-text">${item.description}</div>`
: nothing}
${item.icon
? html`<span slot="start">${item.icon}</span>`
: item.iconPath
? html`<ha-svg-icon
slot="start"
.path=${item.iconPath}
></ha-svg-icon>`
: nothing}
${this.tooltipDescription && item.description
? html`<ha-svg-icon
tabindex="0"
id=${`description-tooltip-${item.key}`}
slot="end"
.path=${mdiInformationOutline}
@click=${stopPropagation}
></ha-svg-icon>
<ha-tooltip
.for=${`description-tooltip-${item.key}`}
@wa-show=${stopPropagation}
@wa-hide=${stopPropagation}
@wa-after-hide=${stopPropagation}
@wa-after-show=${stopPropagation}
>${item.description}</ha-tooltip
> `
: nothing}
<ha-svg-icon
slot="end"
class="plus"
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
`
)}
</ha-md-list>
`;
}
private _renderTarget = memoizeOne((target?: Target) => {
if (!target) {
return nothing;
}
return html`<div class="selected-target">
${this._getSelectedTargetIcon(target[0], target[1])}
<div class="label">${target[2]}</div>
</div>`;
});
private _getSelectedTargetIcon(
targetType: string,
targetId: string | undefined
): TemplateResult | typeof nothing {
if (!targetId) {
return nothing;
}
if (targetType === "floor") {
return html`<ha-floor-icon
.floor=${this.hass.floors[targetId]}
></ha-floor-icon>`;
}
if (targetType === "area" && this.hass.areas[targetId]) {
const area = this.hass.areas[targetId];
if (area.icon) {
return html`<ha-icon .icon=${area.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiTextureBox}></ha-svg-icon>`;
}
if (targetType === "device" && this.hass.devices[targetId]) {
const device = this.hass.devices[targetId];
const configEntry = device.primary_config_entry
? this.configEntryLookup[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
if (domain) {
return html`<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>`;
}
}
if (targetType === "entity" && this.hass.states[targetId]) {
const stateObj = this.hass.states[targetId];
if (stateObj) {
return html`<state-badge
.stateObj=${stateObj}
.hass=${this.hass}
.stateColor=${false}
></state-badge>`;
}
}
if (targetType === "label") {
const label = this.getLabel(targetId);
if (label?.icon) {
return html`<ha-icon .icon=${label.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiLabel}></ha-svg-icon>`;
}
return nothing;
}
private _selected(ev) {
const item = ev.currentTarget;
fireEvent(this, "value-changed", {
value: item.value,
});
}
@eventOptions({ passive: true })
private _onItemsScroll(ev) {
const top = ev.target.scrollTop ?? 0;
this._itemsScrolled = top > 0;
}
public override scrollTo(options?: ScrollToOptions): void;
public override scrollTo(x: number, y: number): void;
public override scrollTo(
xOrOptions?: number | ScrollToOptions,
y?: number
): void {
if (typeof xOrOptions === "number") {
this._itemsDiv?.scrollTo(xOrOptions, y!);
} else {
this._itemsDiv?.scrollTo(xOrOptions);
}
}
static styles = css`
:host {
display: flex;
}
.items {
display: flex;
flex-direction: column;
overflow: auto;
flex: 1;
}
.items.blank {
border-radius: var(--ha-border-radius-xl);
background-color: var(--ha-color-surface-default);
align-items: center;
color: var(--ha-color-text-secondary);
padding: var(--ha-space-0);
margin: var(--ha-space-3) var(--ha-space-4)
max(var(--safe-area-inset-bottom), var(--ha-space-3));
line-height: var(--ha-line-height-expanded);
justify-content: center;
}
.items.error {
background-color: var(--ha-color-fill-danger-quiet-resting);
color: var(--ha-color-on-danger-normal);
}
.items ha-md-list {
--md-list-item-two-line-container-height: var(--ha-space-12);
--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-2);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-family-body);
--ha-md-list-item-gap: var(--ha-space-3);
gap: var(--ha-space-2);
padding: var(--ha-space-0) var(--ha-space-4);
}
.items ha-md-list ha-md-list-item {
border-radius: var(--ha-border-radius-lg);
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.items ha-md-list {
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
}
.items .item-headline {
display: flex;
align-items: center;
gap: var(--ha-space-1);
min-height: var(--ha-space-9);
flex-wrap: wrap;
}
.items-title {
position: sticky;
display: flex;
align-items: center;
font-weight: var(--ha-font-weight-medium);
padding-top: var(--ha-space-2);
padding-bottom: var(--ha-space-2);
padding-inline-start: var(--ha-space-8);
padding-inline-end: var(--ha-space-3);
top: 0;
z-index: 1;
background-color: var(--card-background-color);
}
ha-bottom-sheet .items-title {
padding-top: var(--ha-space-3);
}
.scrolled .items-title:first-of-type {
box-shadow: var(--bar-box-shadow);
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
}
ha-icon-next {
width: var(--ha-space-6);
}
ha-svg-icon.plus {
color: var(--primary-color);
}
.selected-target {
display: inline-flex;
gap: var(--ha-space-1);
justify-content: center;
align-items: center;
border-radius: var(--ha-border-radius-md);
background: var(--ha-color-fill-neutral-normal-resting);
padding: 0 var(--ha-space-2) 0 var(--ha-space-1);
color: var(--ha-color-on-neutral-normal);
overflow: hidden;
}
.selected-target .label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selected-target ha-icon,
.selected-target ha-svg-icon,
.selected-target state-badge,
.selected-target ha-domain-icon {
display: flex;
padding: var(--ha-space-1) 0;
}
.selected-target state-badge {
--mdc-icon-size: 20px;
}
.selected-target state-badge,
.selected-target ha-domain-icon {
width: 24px;
height: 24px;
filter: grayscale(100%);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-add-items": HaAutomationAddItems;
}
}

View File

@@ -1,19 +1,19 @@
import { consume } from "@lit/context";
import {
mdiCancel,
mdiChevronRight,
mdiDelete,
mdiDotsVertical,
mdiMenuDown,
mdiPlus,
mdiTextureBox,
mdiCancel,
mdiDelete,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
@@ -68,8 +68,8 @@ import type {
DeviceRegistryEntry,
} from "../../../data/device_registry";
import {
removeConfigEntryFromDevice,
updateDeviceRegistryEntry,
removeConfigEntryFromDevice,
} from "../../../data/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import {
@@ -86,8 +86,8 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
@@ -318,7 +318,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
})
);
const deviceEntityLookup: DeviceEntityLookup<EntityRegistryEntry> = {};
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;

View File

@@ -1,60 +1,60 @@
import { mdiDownload, mdiPencil } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { mdiPencil, mdiDownload } from "@mdi/js";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-menu-button";
import { goBack, navigate } from "../../common/navigate";
import "../../components/ha-alert";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-list-item";
import "../../components/ha-menu-button";
import "../../components/ha-top-app-bar-fixed";
import "../../components/ha-alert";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../lovelace/components/hui-energy-period-selector";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import { goBack, navigate } from "../../common/navigate";
import type {
BatterySourceTypeEnergyPreference,
DeviceConsumptionEnergyPreference,
EnergyPreferences,
GasSourceTypeEnergyPreference,
GridSourceTypeEnergyPreference,
SolarSourceTypeEnergyPreference,
BatterySourceTypeEnergyPreference,
GasSourceTypeEnergyPreference,
WaterSourceTypeEnergyPreference,
DeviceConsumptionEnergyPreference,
EnergyCollection,
} from "../../data/energy";
import {
computeConsumptionData,
getEnergyDataCollection,
getSummedData,
} from "../../data/energy";
import { fileDownload } from "../../util/file_download";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
import type { StatisticValue } from "../../data/recorder";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { fileDownload } from "../../util/file_download";
import "../lovelace/components/hui-energy-period-selector";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
const ENERGY_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
},
{
strategy: {
type: "energy-electricity",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
path: "electricity",
},
{
type: "panel",
path: "setup",
cards: [{ type: "custom:energy-setup-wizard-card" }],
},
],
const OVERVIEW_VIEW = {
strategy: {
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
};
const ELECTRICITY_VIEW = {
back_path: "/energy",
path: "electricity",
strategy: {
type: "energy-electricity",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceViewConfig;
const WIZARD_VIEW = {
type: "panel",
path: "setup",
cards: [{ type: "custom:energy-setup-wizard-card" }],
};
@customElement("ha-panel-energy")
@@ -74,7 +74,8 @@ class PanelEnergy extends LitElement {
prefix: string;
};
private _energyCollection?: EnergyCollection;
@state()
private _config?: LovelaceConfig;
get _viewPath(): string | undefined {
const viewPath: string | undefined = this.route!.path.split("/")[1];
@@ -83,7 +84,7 @@ class PanelEnergy extends LitElement {
public connectedCallback() {
super.connectedCallback();
this._loadPrefs();
this._loadLovelaceConfig();
}
public async willUpdate(changedProps: PropertyValues) {
@@ -101,32 +102,15 @@ class PanelEnergy extends LitElement {
}
}
private async _loadPrefs() {
if (this._viewPath === "setup") {
await import("./cards/energy-setup-wizard-card");
} else {
this._energyCollection = getEnergyDataCollection(this.hass, {
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
// Have to manually refresh here as we don't want to subscribe yet
await this._energyCollection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
navigate("/energy/setup");
}
this._error = err.message;
return;
}
const prefs = this._energyCollection.prefs!;
if (
prefs.device_consumption.length === 0 &&
prefs.energy_sources.length === 0
) {
// No energy sources available, start from scratch
navigate("/energy/setup");
}
private async _loadLovelaceConfig() {
try {
this._config = undefined;
this._config = await this._generateLovelaceConfig();
} catch (err) {
this._error = (err as Error).message;
}
this._setLovelace();
}
private _back(ev) {
@@ -135,25 +119,22 @@ class PanelEnergy extends LitElement {
}
protected render() {
if (!this._energyCollection?.prefs) {
if (!this._config && !this._error) {
// Still loading
return html`<div class="centered">
<ha-spinner size="large"></ha-spinner>
</div>`;
return html`
<div class="centered">
<ha-spinner size="large"></ha-spinner>
</div>
`;
}
const { prefs } = this._energyCollection;
const isSingleView = prefs.energy_sources.every((source) =>
["grid", "solar", "battery"].includes(source.type)
);
let viewPath = this._viewPath;
if (isSingleView) {
// if only electricity sources, show electricity view directly
viewPath = "electricity";
}
const viewIndex = Math.max(
ENERGY_LOVELACE_CONFIG.views.findIndex((view) => view.path === viewPath),
0
);
const isSingleView = this._config?.views.length === 1;
const viewPath = this._viewPath;
const viewIndex = this._config
? Math.max(
this._config.views.findIndex((view) => view.path === viewPath),
0
)
: 0;
const showBack =
this._searchParms.has("historyBack") || (!isSingleView && viewIndex > 0);
@@ -229,10 +210,56 @@ class PanelEnergy extends LitElement {
`;
}
private _fetchEnergyPrefs = async (): Promise<
EnergyPreferences | undefined
> => {
const collection = getEnergyDataCollection(this.hass, {
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
await collection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
return undefined;
}
throw err;
}
return collection.prefs;
};
private async _generateLovelaceConfig(): Promise<LovelaceConfig> {
const prefs = await this._fetchEnergyPrefs();
if (
!prefs ||
(prefs.device_consumption.length === 0 &&
prefs.energy_sources.length === 0)
) {
await import("./cards/energy-setup-wizard-card");
return {
views: [WIZARD_VIEW],
};
}
const isElectricityOnly = prefs.energy_sources.every((source) =>
["grid", "solar", "battery"].includes(source.type)
);
if (isElectricityOnly) {
return {
views: [ELECTRICITY_VIEW],
};
}
return {
views: [OVERVIEW_VIEW, ELECTRICITY_VIEW],
};
}
private _setLovelace() {
if (!this._config) {
return;
}
this._lovelace = {
config: ENERGY_LOVELACE_CONFIG,
rawConfig: ENERGY_LOVELACE_CONFIG,
config: this._config,
rawConfig: this._config,
editMode: false,
urlPath: "energy",
mode: "generated",
@@ -252,7 +279,9 @@ class PanelEnergy extends LitElement {
private async _dumpCSV(ev) {
ev.stopPropagation();
const energyData = this._energyCollection!;
const energyData = getEnergyDataCollection(this.hass, {
key: "energy_dashboard",
});
if (!energyData.prefs || !energyData.state.stats) {
return;
@@ -549,12 +578,7 @@ class PanelEnergy extends LitElement {
}
private _reloadView() {
// Force strategy to be re-run by making a copy of the view
const config = this._lovelace!.config;
this._lovelace = {
...this._lovelace!,
config: { ...config, views: config.views.map((view) => ({ ...view })) },
};
this._loadLovelaceConfig();
}
static get styles(): CSSResultGroup {

View File

@@ -46,7 +46,7 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
iframe: () => import("./iframe/iframe-view-strategy"),
area: () => import("./areas/area-view-strategy"),
"areas-overview": () => import("./areas/areas-overview-view-strategy"),
"home-main": () => import("./home/home-main-view-strategy"),
"home-overview": () => import("./home/home-overview-view-strategy"),
"home-media-players": () =>
import("./home/home-media-players-view-strategy"),
"home-area": () => import("./home/home-area-view-strategy"),

View File

@@ -10,7 +10,7 @@ import {
HOME_SUMMARIES_ICONS,
} from "./helpers/home-summaries";
import type { HomeAreaViewStrategyConfig } from "./home-area-view-strategy";
import type { HomeMainViewStrategyConfig } from "./home-main-view-strategy";
import type { HomeOverviewViewStrategyConfig } from "./home-overview-view-strategy";
export interface HomeDashboardStrategyConfig {
type: "home";
@@ -75,11 +75,11 @@ export class HomeDashboardStrategy extends ReactiveElement {
views: [
{
icon: "mdi:home",
path: "home",
path: "overview",
strategy: {
type: "home-main",
type: "home-overview",
favorite_entities: config.favorite_entities,
} satisfies HomeMainViewStrategyConfig,
} satisfies HomeOverviewViewStrategyConfig,
},
...areaViews,
mediaPlayersView,

View File

@@ -26,8 +26,8 @@ import type { CommonControlSectionStrategyConfig } from "../usage_prediction/com
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
export interface HomeMainViewStrategyConfig {
type: "home-main";
export interface HomeOverviewViewStrategyConfig {
type: "home-overview";
favorite_entities?: string[];
}
@@ -57,10 +57,10 @@ const computeAreaCard = (
};
};
@customElement("home-main-view-strategy")
export class HomeMainViewStrategy extends ReactiveElement {
@customElement("home-overview-view-strategy")
export class HomeOverviewViewStrategy extends ReactiveElement {
static async generate(
config: HomeMainViewStrategyConfig,
config: HomeOverviewViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = Object.values(hass.areas);
@@ -325,6 +325,6 @@ export class HomeMainViewStrategy extends ReactiveElement {
declare global {
interface HTMLElementTagNameMap {
"home-main-view-strategy": HomeMainViewStrategy;
"home-overview-view-strategy": HomeOverviewViewStrategy;
}
}

View File

@@ -52,8 +52,6 @@ export const waColorStyles = css`
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
--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-surface-default: var(--card-background-color);
--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-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
--wa-color-text-normal: var(--ha-color-text-primary);
}
`;

View File

@@ -9,16 +9,12 @@ export const waMainStyles = css`
--wa-focus-ring-offset: 2px;
--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-xl: var(--ha-space-8);
--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-transition-normal: 150ms;
--wa-transition-fast: 75ms;
--wa-transition-easing: ease;
@@ -32,7 +28,6 @@ export const waMainStyles = css`
--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-m: var(--ha-box-shadow-m);
--wa-shadow-l: var(--ha-box-shadow-l);

View File

@@ -4010,25 +4010,7 @@
"item_pasted": "{item} pasted",
"ctrl": "Ctrl",
"del": "Del",
"targets": "Targets",
"select_target": "Select a target",
"home": "Home",
"unassigned": "Unassigned",
"blocks": "Blocks",
"show_more": "Show more",
"unassigned_entities": "Unassigned entities",
"unassigned_devices": "Unassigned devices",
"empty_section_search": {
"block": "No blocks found for {term}",
"entity": "No entities found for {term}",
"device": "No devices found for {term}",
"area": "No areas or floors found for {term}",
"label": "No labels found for {term}"
},
"load_target_items_failed": "Failed to load target items for",
"other_areas": "Other areas",
"services": "Services",
"helpers": "Helpers",
"triggers": {
"name": "Triggers",
"header": "When",
@@ -4036,10 +4018,7 @@
"learn_more": "Learn more about triggers",
"triggered": "Triggered",
"add": "Add trigger",
"empty_search": {
"global": "No triggers and targets found for {term}",
"item": "No triggers found for {term}"
},
"empty_search": "No triggers found for {term}",
"id": "Trigger ID",
"optional": "Optional",
"edit_id": "Edit ID",
@@ -4060,7 +4039,6 @@
"copied_to_clipboard": "Trigger copied to clipboard",
"cut_to_clipboard": "Trigger cut to clipboard",
"select": "Select a trigger",
"no_items_for_target": "No triggers available for",
"groups": {
"device": {
"label": "Device"
@@ -4302,10 +4280,7 @@
"description": "All conditions added here need to be satisfied for the automation to run. A condition can be satisfied or not at any given time, for example: ''If {user} is home''. You can use building blocks to create more complex conditions.",
"learn_more": "Learn more about conditions",
"add": "Add condition",
"empty_search": {
"global": "No conditions, blocks and targets found for {term}",
"item": "No conditions found for {term}"
},
"empty_search": "No conditions and blocks found for {term}",
"add_building_block": "Add building block",
"test": "Test",
"testing_error": "Condition did not pass",
@@ -4328,7 +4303,6 @@
"copied_to_clipboard": "Condition copied to clipboard",
"cut_to_clipboard": "Condition cut to clipboard",
"select": "Select a condition",
"no_items_for_target": "No conditions available for",
"groups": {
"device": {
"label": "Device"
@@ -4474,10 +4448,7 @@
"description": "All actions added here will be performed in sequence when the automation runs. An action usually controls one of your areas, devices, or entities, for example: 'Turn on the lights'. You can use building blocks to create more complex sequences of actions.",
"learn_more": "Learn more about actions",
"add": "Add action",
"empty_search": {
"global": "No actions, blocks and targets found for {term}",
"item": "No actions found for {term}"
},
"empty_search": "No actions and blocks found for {term}",
"add_building_block": "Add building block",
"invalid_action": "Invalid action",
"run": "Run action",
@@ -4502,7 +4473,6 @@
"copied_to_clipboard": "Action copied to clipboard",
"cut_to_clipboard": "Action cut to clipboard",
"select": "Select an action",
"no_items_for_target": "No actions available for",
"groups": {
"device_id": {
"label": "Device"

View File

@@ -1940,9 +1940,9 @@ __metadata:
languageName: node
linkType: hard
"@home-assistant/webawesome@npm:3.0.0-ha.0":
version: 3.0.0-ha.0
resolution: "@home-assistant/webawesome@npm:3.0.0-ha.0"
"@home-assistant/webawesome@npm:3.0.0":
version: 3.0.0
resolution: "@home-assistant/webawesome@npm:3.0.0"
dependencies:
"@ctrl/tinycolor": "npm:4.1.0"
"@floating-ui/dom": "npm:^1.6.13"
@@ -1953,7 +1953,7 @@ __metadata:
lit: "npm:^3.2.1"
nanoid: "npm:^5.1.5"
qr-creator: "npm:^1.0.0"
checksum: 10/2034d498d5b26bb0573ebc2c9aadd144604bb48c04becbae0c67b16857d8e5d6562626e795974362c3fc41e9b593a9005595d8b5ff434b1569b2d724af13043b
checksum: 10/03400894cfee8548fd5b1f5c56d31d253830e704b18ba69d36ce6b761d8b1bef2fb52cffba8d9b033033bb582f2f51a2d6444d82622f66d70150e2104fcb49e2
languageName: node
linkType: hard
@@ -9226,7 +9226,7 @@ __metadata:
"@fullcalendar/list": "npm:6.1.19"
"@fullcalendar/luxon3": "npm:6.1.19"
"@fullcalendar/timegrid": "npm:6.1.19"
"@home-assistant/webawesome": "npm:3.0.0-ha.0"
"@home-assistant/webawesome": "npm:3.0.0"
"@lezer/highlight": "npm:1.2.3"
"@lit-labs/motion": "npm:1.0.9"
"@lit-labs/observers": "npm:2.0.6"