Compare commits

..
Author SHA1 Message Date
Wendelin 7c80ea10d1 Migrate config-entries to list-base 2026-09-16 15:27:25 +02:00
Wendelin ea55ceebfe Migrate profile 2026-09-16 10:00:39 +02:00
Wendelin 51ded992dd Fix network info secondary text size 2026-09-16 09:10:02 +02:00
96 changed files with 793 additions and 1344 deletions
+1
View File
@@ -3,6 +3,7 @@ import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content";
import "../ha-demo-options";
import type { HomeAssistant } from "../../../src/types";
import { computeShowNewMoreInfo } from "../../../src/dialogs/more-info/const";
+2
View File
@@ -1,7 +1,9 @@
import { html, css, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "../../src/components/ha-icon-button";
import "../../src/managers/notification-manager";
import { haStyle } from "../../src/resources/styles";
import "./components/page-description";
@customElement("ha-demo-options")
class HaDemoOptions extends LitElement {
@@ -19,16 +19,37 @@ import {
getEntityContext,
getEntityEntryContext,
} from "./context/get_entity_context";
import type { EntityNameItem, EntityNameOptions } from "./entity_name_config";
const DEFAULT_SEPARATOR = " ";
export { DEFAULT_ENTITY_NAME, ENTITY_NAME_TYPES } from "./entity_name_config";
export type {
EntityNameItem,
EntityNameOptions,
EntityNameType,
} from "./entity_name_config";
export const DEFAULT_ENTITY_NAME = [
{ type: "parent_device" },
{ type: "device" },
{ type: "entity" },
] satisfies EntityNameItem[];
export const ENTITY_NAME_TYPES = [
"floor",
"area",
"parent_device",
"device",
"entity",
] as const;
export type EntityNameType = (typeof ENTITY_NAME_TYPES)[number];
export type EntityNameItem =
| {
type: EntityNameType;
}
| {
type: "text";
text: string;
};
export interface EntityNameOptions {
separator?: string;
}
// Joins items that need no entity context. Returns undefined as soon as one
// item references the registries (device, area, ...).
-34
View File
@@ -1,34 +0,0 @@
/**
* Shape of a composed entity name, kept free of runtime dependencies so that
* modules in the core entry chunk can name an entity without pulling in the
* registry-aware formatters from `compute_entity_name_display`.
*/
export const ENTITY_NAME_TYPES = [
"floor",
"area",
"parent_device",
"device",
"entity",
] as const;
export type EntityNameType = (typeof ENTITY_NAME_TYPES)[number];
export type EntityNameItem =
| {
type: EntityNameType;
}
| {
type: "text";
text: string;
};
export interface EntityNameOptions {
separator?: string;
}
export const DEFAULT_ENTITY_NAME = [
{ type: "parent_device" },
{ type: "device" },
{ type: "entity" },
] satisfies EntityNameItem[];
@@ -22,6 +22,7 @@ import "./ha-list";
import "./ha-list-item";
import "./voice-assistant-brand-icon";
import { voiceAssistants } from "../data/expose";
import "../panels/config/voice-assistants/expose/expose-assistant-icon";
@customElement("ha-filter-voice-assistants")
export class HaFilterVoiceAssistants extends LitElement {
@@ -3,6 +3,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
import "../ha-check-list-item";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-dropdown";
@@ -33,6 +33,7 @@ import "../ha-list";
import "../ha-spinner";
import "../ha-svg-icon";
import "../ha-tip";
import "./ha-media-player-browse";
import "./ha-media-upload-button";
import type { MediaManageDialogParams } from "./show-media-manage-dialog";
@@ -19,6 +19,7 @@ import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../../../types";
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
import "../../ha-adaptive-dialog";
import "../../ha-dialog-header";
import "../../ha-icon-button";
import "../../ha-icon-next";
import "../../ha-svg-icon";
+7
View File
@@ -21,6 +21,7 @@ import {
} from "../common/datetime/calc_date";
import type { DateRange } from "../common/datetime/calc_date_range";
import { calcDateRange } from "../common/datetime/calc_date_range";
import { DEFAULT_ENTITY_NAME } from "../common/entity/compute_entity_name_display";
import { formatNumber } from "../common/number/format_number";
import { normalizeValueBySIPrefix } from "../common/number/normalize-by-si-prefix";
import { groupBy } from "../common/util/group-by";
@@ -327,6 +328,12 @@ export const computeEnergyLabel = (
return customName;
}
const stateObj = hass.states[statisticId];
if (stateObj) {
return hass.formatEntityName(stateObj, DEFAULT_ENTITY_NAME);
}
return getStatisticLabel(hass, statisticId, statisticsMetaData);
};
+2 -3
View File
@@ -1,5 +1,5 @@
import type { Connection } from "home-assistant-js-websocket";
import { DEFAULT_ENTITY_NAME } from "../common/entity/entity_name_config";
import { computeStateName } from "../common/entity/compute_state_name";
import type { HaDurationData } from "../components/ha-duration-input";
import type { HomeAssistant } from "../types";
import { firstWeekday } from "../common/datetime/first_weekday";
@@ -348,9 +348,8 @@ export const getStatisticLabel = (
): string => {
const entity = hass.states[statisticsId];
if (entity) {
return hass.formatEntityName(entity, DEFAULT_ENTITY_NAME);
return computeStateName(entity);
}
// External statistics have no entity to resolve a name against.
return statisticsMetaData?.name || statisticsId;
};
+1 -19
View File
@@ -1,6 +1,4 @@
import { ensureArray } from "../common/array/ensure-array";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import type { Context, HomeAssistant, ServiceCallRequest } from "../types";
import type { Context, HomeAssistant } from "../types";
import type { Action } from "./script";
export const callExecuteScript = (
@@ -24,19 +22,3 @@ export const serviceCallWillDisconnect = (
"update.home_assistant_core_update",
"update.home_assistant_operating_system_update",
].includes(serviceData?.entity_id));
// Core merges the target into the service data, so a target entity_id
// replaces the legacy service data one rather than adding to it. Its schema
// also accepts comma separated ids and lowercases them.
export const getServiceCallEntityIds = (
serviceData?: ServiceCallRequest["serviceData"],
target?: ServiceCallRequest["target"]
): string[] => [
...new Set(
(ensureArray(target?.entity_id ?? serviceData?.entity_id) ?? [])
.filter((id): id is string => typeof id === "string")
.flatMap((id) => id.split(","))
.map((id) => id.trim().toLowerCase())
.filter(isValidEntityId)
),
];
@@ -17,6 +17,7 @@ import type { LocalizeFunc } from "../../../../common/translations/localize";
import { throttle } from "../../../../common/util/throttle";
import "../../../../components/ha-hs-color-picker";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-labeled-slider";
import { apiContext } from "../../../../data/context";
import type { LightColor, LightEntity } from "../../../../data/light";
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { transform } from "../../../common/decorators/transform";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { apiContext, internationalizationContext } from "../../../data/context";
import { setDateValue } from "../../../data/date";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
@@ -11,7 +11,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-attribute-icon";
@@ -39,6 +38,7 @@ import "../../../state-control/light/ha-state-control-light-brightness";
import { apiContext, formattersContext } from "../../../data/context";
import "../components/ha-more-info-control-select-container";
import "../components/ha-more-info-state-header";
import "../components/lights/ha-favorite-color-button";
import "../components/lights/ha-more-info-light-favorite-colors";
import "../components/lights/light-color-rgb-picker";
import "../components/lights/light-color-temp-picker";
@@ -86,26 +86,12 @@ class MoreInfoLight extends LitElement {
private _setMainControl(ev: any) {
ev.stopPropagation();
this._changeMainControl(ev.currentTarget.control);
this._mainControl = ev.currentTarget.control;
}
private _resetMainControl(ev: any) {
ev.stopPropagation();
this._changeMainControl("brightness");
}
public connectedCallback(): void {
super.connectedCallback();
// A container that outlives this control (e.g. more-info-content when the
// dialog moves between entities) resyncs with the default control.
fireEvent(this, "light-main-control-changed", {
control: this._mainControl,
});
}
private _changeMainControl(control: MainControl) {
this._mainControl = control;
fireEvent(this, "light-main-control-changed", { control });
this._mainControl = "brightness";
}
private get _stateOverride() {
@@ -414,14 +400,4 @@ declare global {
interface HTMLElementTagNameMap {
"more-info-light": MoreInfoLight;
}
interface HASSDomEvents {
"light-main-control-changed": { control: MainControl };
}
interface HTMLElementEventMap {
"light-main-control-changed": HASSDomEvent<
HASSDomEvents["light-main-control-changed"]
>;
}
}
@@ -10,6 +10,7 @@ import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-outlined-icon-button";
import "../../../components/ha-state-icon";
import { apiContext } from "../../../data/context";
import type { LockEntity } from "../../../data/lock";
@@ -31,6 +31,7 @@ import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-marquee-text";
import "../../../components/ha-select";
import type { HaSlider } from "../../../components/ha-slider";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
@@ -5,9 +5,11 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { listenMediaQuery } from "../../../common/dom/media_query";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import "../../../components/entity/state-info";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-markdown";
import "../../../components/ha-relative-time";
import "../../../components/ha-service-control";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
@@ -3,6 +3,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { transform } from "../../../common/decorators/transform";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { apiContext, internationalizationContext } from "../../../data/context";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
@@ -6,8 +6,10 @@ import { consumeLocalize } from "../../../common/decorators/consume-context-entr
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { sanitizeHttpUrl } from "../../../common/url/sanitize-http-url";
import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-faded";
import "../../../components/ha-markdown";
import "../../../components/ha-spinner";
import "../../../components/progress/ha-progress-bar";
+88 -119
View File
@@ -1,7 +1,6 @@
import type { HassEntity } 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 { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { computeEntityName } from "../../common/entity/compute_entity_name";
@@ -12,7 +11,6 @@ import "../../components/ha-badge";
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
import { supportsCoverPositionCardFeature } from "../../panels/lovelace/card-features/hui-cover-position-card-feature";
import { supportsLightBrightnessCardFeature } from "../../panels/lovelace/card-features/hui-light-brightness-card-feature";
import { supportsLightColorTempCardFeature } from "../../panels/lovelace/card-features/hui-light-color-temp-card-feature";
import type { LovelaceCardFeatureConfig } from "../../panels/lovelace/card-features/types";
import type { TileCardConfig } from "../../panels/lovelace/cards/types";
import { importMoreInfoControl } from "../../panels/lovelace/custom-card-helpers";
@@ -27,8 +25,6 @@ interface EntityInfo {
deviceId: string | undefined;
}
type LightMainControl = HASSDomEvents["light-main-control-changed"]["control"];
@customElement("more-info-content")
class MoreInfoContent extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@@ -41,17 +37,6 @@ class MoreInfoContent extends LitElement {
@property({ attribute: false }) public data?: Record<string, any>;
// Mirrors the mode selected in the light control so group members
// show the matching slider.
@state() private _lightMainControl: LightMainControl = "brightness";
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this.addEventListener("light-main-control-changed", (ev) => {
this._lightMainControl = ev.detail.control;
});
}
protected render() {
let moreInfoType: string | undefined;
@@ -88,10 +73,7 @@ class MoreInfoContent extends LitElement {
? html`
<hui-section
.hass=${this.hass}
.config=${this._entitiesSectionConfig(
memberIds,
this._lightMainControl
)}
.config=${this._entitiesSectionConfig(memberIds)}
>
</hui-section>
`
@@ -122,110 +104,97 @@ class MoreInfoContent extends LitElement {
}
);
private _entitiesSectionConfig = memoizeOne(
(entityIds: string[], lightMainControl: LightMainControl) => {
const hass = this.hass!;
private _entitiesSectionConfig = memoizeOne((entityIds: string[]) => {
const hass = this.hass!;
// Get entity names and areas for all visible entities
const entityInfos = entityIds
.map<EntityInfo | null>((entityId) => {
const entry = hass.entities[entityId];
if (entry?.hidden) {
return null;
}
const stateObj = hass.states[entityId];
if (!stateObj) {
return null;
}
const entityName = computeEntityName(
stateObj,
hass.entities,
hass.devices
);
const { area, device } = getEntityContext(
stateObj,
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
const areaId = area?.area_id;
const deviceId = device?.id;
return { entityId, entityName, areaId, deviceId };
})
.filter(Boolean) as EntityInfo[];
// Check if all entities have the same entity name
const entityNames = new Set(entityInfos.map((info) => info.entityName));
const allSameEntityName = entityNames.size === 1;
// Check if all entities have the same area
const areaIds = new Set(entityInfos.map((info) => info.areaId));
const allSameArea = areaIds.size === 1;
// Check if all entities belong to the same device
const deviceIds = new Set(entityInfos.map((info) => info.deviceId));
const allSameDevice = deviceIds.size === 1;
// Build name and state content config based on conditions. The device name
// is redundant when every member belongs to the same device, so omit it
// (and fall back to the entity name so the tile still has a label).
const name: EntityNameItem[] = [];
if (!allSameDevice) {
name.push({ type: "device" });
}
if (!allSameEntityName || allSameDevice) {
name.push({ type: "entity" });
}
const stateContent = ["state"];
if (!allSameArea) {
stateContent.push("area_name");
}
const cards = entityInfos.map(({ entityId }) => {
const features: LovelaceCardFeatureConfig[] = [];
const context = { entity_id: entityId };
if (supportsCoverPositionCardFeature(hass, context)) {
features.push({
type: "cover-position",
});
} else if (lightMainControl === "color") {
// There is no RGB tile feature yet, so when the group is set to
// color the members show no control rather than a mismatched
// brightness slider.
} else if (
lightMainControl === "color_temp" &&
supportsLightColorTempCardFeature(hass, context)
) {
features.push({
type: "light-color-temp",
});
} else if (supportsLightBrightnessCardFeature(hass, context)) {
features.push({
type: "light-brightness",
});
// Get entity names and areas for all visible entities
const entityInfos = entityIds
.map<EntityInfo | null>((entityId) => {
const entry = hass.entities[entityId];
if (entry?.hidden) {
return null;
}
const stateObj = hass.states[entityId];
if (!stateObj) {
return null;
}
const entityName = computeEntityName(
stateObj,
hass.entities,
hass.devices
);
const { area, device } = getEntityContext(
stateObj,
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
const areaId = area?.area_id;
const deviceId = device?.id;
return { entityId, entityName, areaId, deviceId };
})
.filter(Boolean) as EntityInfo[];
return {
type: "tile",
entity: entityId,
name,
state_content: stateContent,
features_position: "inline",
features,
grid_options: { columns: 12 },
} as TileCardConfig;
});
// Check if all entities have the same entity name
const entityNames = new Set(entityInfos.map((info) => info.entityName));
const allSameEntityName = entityNames.size === 1;
// Check if all entities have the same area
const areaIds = new Set(entityInfos.map((info) => info.areaId));
const allSameArea = areaIds.size === 1;
// Check if all entities belong to the same device
const deviceIds = new Set(entityInfos.map((info) => info.deviceId));
const allSameDevice = deviceIds.size === 1;
// Build name and state content config based on conditions. The device name
// is redundant when every member belongs to the same device, so omit it
// (and fall back to the entity name so the tile still has a label).
const name: EntityNameItem[] = [];
if (!allSameDevice) {
name.push({ type: "device" });
}
if (!allSameEntityName || allSameDevice) {
name.push({ type: "entity" });
}
const stateContent = ["state"];
if (!allSameArea) {
stateContent.push("area_name");
}
const cards = entityInfos.map(({ entityId }) => {
const features: LovelaceCardFeatureConfig[] = [];
const context = { entity_id: entityId };
if (supportsCoverPositionCardFeature(hass, context)) {
features.push({
type: "cover-position",
});
} else if (supportsLightBrightnessCardFeature(hass, context)) {
features.push({
type: "light-brightness",
});
}
return {
type: "grid",
cards,
};
}
);
type: "tile",
entity: entityId,
name,
state_content: stateContent,
features_position: "inline",
features,
grid_options: { columns: 12 },
} as TileCardConfig;
});
return {
type: "grid",
cards,
};
});
static styles = css`
hui-section {
@@ -27,6 +27,7 @@ import { updateDeviceRegistryEntry } from "../../data/device/device_registry";
import type { InputSelectEntity } from "../../data/input_select";
import { setSelectOption } from "../../data/select";
import { showVoiceAssistantPipelineDetailDialog } from "../../panels/config/voice-assistants/show-dialog-voice-assistant-pipeline-detail";
import "../../panels/lovelace/entity-rows/hui-select-entity-row";
import type { HomeAssistant } from "../../types";
import { getTranslation } from "../../util/common-translation";
import { AssistantSetupStyles } from "./styles";
@@ -6,6 +6,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import "../../components/ha-button";
import "../../components/ha-dialog-header";
import "../../components/ha-spinner";
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
import { interceptWakeWord } from "../../data/assist_satellite";
-10
View File
@@ -184,15 +184,6 @@ interface EMOutgoingMessageAddEntityTo extends EMMessage {
};
}
interface EMOutgoingMessageEntityControlled extends EMMessage {
type: "entity/controlled";
payload: {
entity_ids: string[];
domain: string;
service: string;
};
}
interface EMOutgoingMessageMoreInfoOpened extends EMMessage {
type: "more_info/opened";
payload: {
@@ -248,7 +239,6 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageImprovScan
| EMOutgoingMessageImprovConfigureDevice
| EMOutgoingMessageAddEntityTo
| EMOutgoingMessageEntityControlled
| EMOutgoingMessageFocusElement
| EMOutgoingMessageReloadAndClearCache
| EMOutgoingMessageAssistSettings;
@@ -12,12 +12,15 @@ import { isDate } from "../../common/string/is_date";
import "../../components/entity/state-info";
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-date-input";
import "../../components/ha-dialog-footer";
import "../../components/ha-time-input";
import "../../components/ha-dialog";
import type { CalendarEventMutableParams } from "../../data/calendar";
import { deleteCalendarEvent } from "../../data/calendar";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../lovelace/components/hui-generic-entity-row";
import { renderRRuleAsText } from "./recurrence";
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
import type { CalendarEventDetailDialogParams } from "./show-dialog-calendar-event-detail";
@@ -42,6 +42,7 @@ import {
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../lovelace/components/hui-generic-entity-row";
import "./ha-recurrence-rule-editor";
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
import type { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor";
@@ -8,7 +8,9 @@ import type {
LocalizeKeys,
} from "../../../../../common/translations/localize";
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
import "../../../../../components/ha-dropdown-item";
import type { PickerComboBoxItem } from "../../../../../components/ha-picker-combo-box";
import "../../../../../components/ha-select";
import {
DYNAMIC_PREFIX,
getValueFromDynamic,
@@ -3,6 +3,7 @@ import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-service-picker";
import "../../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor";
import "../../../../../components/input/ha-input";
@@ -27,6 +27,7 @@ import {
import "../../../../components/chips/ha-chip-set";
import "../../../../components/chips/ha-filter-chip";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-button-toggle-group";
import "../../../../components/ha-combo-box-item";
import "../../../../components/ha-domain-icon";
import "../../../../components/ha-floor-icon";
@@ -13,13 +13,9 @@ import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import {
fireEvent,
type HASSDomTargetEvent,
} from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -53,14 +49,9 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import "../../../components/ha-trace-picker";
import "../../../components/ha-split-panel";
import type { HaSplitPanel } from "../../../components/ha-split-panel";
const TABS = ["details", "timeline", "logbook", "automation_config"] as const;
const STORAGE_KEY_SPLIT_POSITION = "automation-trace-split-position";
const DEFAULT_SPLIT_POSITION = 20;
@customElement("ha-automation-trace")
export class HaAutomationTrace extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -93,8 +84,6 @@ export class HaAutomationTrace extends LitElement {
@state() private _view: (typeof TABS)[number] | "blueprint" = "details";
@state() private _splitPosition = DEFAULT_SPLIT_POSITION;
@query("hat-script-graph") private _graph?: HatScriptGraph;
protected render(): TemplateResult {
@@ -102,6 +91,10 @@ export class HaAutomationTrace extends LitElement {
? this.hass.states[this._entityId]
: undefined;
const graph = this._graph;
const trackedNodes = graph?.trackedNodes;
const renderedNodes = graph?.renderedNodes;
const title = stateObj?.attributes.friendly_name || this._entityId;
let devButtons: TemplateResult | string = "";
@@ -247,19 +240,94 @@ export class HaAutomationTrace extends LitElement {
? ""
: html`
<div class="main">
${
this.narrow
? this._renderPanes()
: html`
<ha-split-panel
class="split"
.position=${this._splitPosition}
@wa-reposition=${this._splitRepositioned}
<div class="graph">
<hat-script-graph
.trace=${this._trace}
.selected=${this._selected?.path}
@graph-node-selected=${this._pickNode}
></hat-script-graph>
</div>
<div class="info">
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
${TABS.map(
(view) => html`
<ha-tab-group-tab
slot="nav"
.active=${this._view === view}
.panel=${view}
>
${this._renderPanes()}
</ha-split-panel>
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.${view}`
)}
</ha-tab-group-tab>
`
}
)}
${
this._trace.blueprint_inputs
? html`
<ha-tab-group-tab
slot="nav"
.active=${this._view === "blueprint"}
panel="blueprint"
>
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.blueprint_config`
)}
</ha-tab-group-tab>
`
: ""
}
</ha-tab-group>
${
this._selected === undefined ||
this._logbookEntries === undefined ||
trackedNodes === undefined
? nothing
: this._view === "details"
? html`
<ha-trace-path-details
.hass=${this.hass}
.narrow=${this.narrow}
.trace=${this._trace}
.selected=${this._selected}
.logbookEntries=${this._logbookEntries}
.trackedNodes=${trackedNodes}
.renderedNodes=${renderedNodes!}
></ha-trace-path-details>
`
: this._view === "automation_config"
? html`
<ha-trace-config
.trace=${this._trace}
></ha-trace-config>
`
: this._view === "logbook"
? html`
<ha-trace-logbook
.hass=${this.hass}
.narrow=${this.narrow}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries}
></ha-trace-logbook>
`
: this._view === "blueprint"
? html`
<ha-trace-blueprint-config
.trace=${this._trace}
></ha-trace-blueprint-config>
`
: html`
<ha-trace-timeline
.hass=${this.hass}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries}
.selected=${this._selected}
@value-changed=${this._timelinePathPicked}
></ha-trace-timeline>
`
}
</div>
</div>
`
}
@@ -267,115 +335,12 @@ export class HaAutomationTrace extends LitElement {
`;
}
private _renderPanes(): TemplateResult {
const graph = this._graph;
const trackedNodes = graph?.trackedNodes;
const renderedNodes = graph?.renderedNodes;
return html`
<div class="graph" slot="start">
<hat-script-graph
.trace=${this._trace}
.selected=${this._selected?.path}
@graph-node-selected=${this._pickNode}
></hat-script-graph>
</div>
<div class="info" slot="end">
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
${TABS.map(
(view) => html`
<ha-tab-group-tab
slot="nav"
.active=${this._view === view}
.panel=${view}
>
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.${view}`
)}
</ha-tab-group-tab>
`
)}
${
this._trace!.blueprint_inputs
? html`
<ha-tab-group-tab
slot="nav"
.active=${this._view === "blueprint"}
panel="blueprint"
>
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.blueprint_config`
)}
</ha-tab-group-tab>
`
: ""
}
</ha-tab-group>
${
this._selected === undefined ||
this._logbookEntries === undefined ||
trackedNodes === undefined
? nothing
: this._view === "details"
? html`
<ha-trace-path-details
.hass=${this.hass}
.narrow=${this.narrow}
.trace=${this._trace}
.selected=${this._selected}
.logbookEntries=${this._logbookEntries}
.trackedNodes=${trackedNodes}
.renderedNodes=${renderedNodes!}
></ha-trace-path-details>
`
: this._view === "automation_config"
? html`
<ha-trace-config .trace=${this._trace}></ha-trace-config>
`
: this._view === "logbook"
? html`
<ha-trace-logbook
.hass=${this.hass}
.narrow=${this.narrow}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries}
></ha-trace-logbook>
`
: this._view === "blueprint"
? html`
<ha-trace-blueprint-config
.trace=${this._trace}
></ha-trace-blueprint-config>
`
: html`
<ha-trace-timeline
.hass=${this.hass}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries}
.selected=${this._selected}
@value-changed=${this._timelinePathPicked}
></ha-trace-timeline>
`
}
</div>
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("triggers");
this.hass.loadBackendTranslation("conditions");
const storedPosition = localStorage?.[STORAGE_KEY_SPLIT_POSITION];
if (storedPosition) {
const parsed = parseFloat(storedPosition);
if (!isNaN(parsed) && parsed > 0 && parsed < 100) {
this._splitPosition = parsed;
}
}
if (!this.automationId) {
return;
}
@@ -463,19 +428,6 @@ export class HaAutomationTrace extends LitElement {
this._selected = ev.detail;
}
private _splitRepositioned(ev: HASSDomTargetEvent<HaSplitPanel>) {
this._splitPosition = ev.target.position;
this._storeSplitPosition();
}
private _storeSplitPosition = debounce(
() => {
localStorage[STORAGE_KEY_SPLIT_POSITION] = String(this._splitPosition);
},
500,
false
);
private _refreshTraces() {
this._loadTraces();
}
@@ -666,22 +618,13 @@ export class HaAutomationTrace extends LitElement {
padding: 16px;
}
ha-split-panel.split {
flex: 1;
min-height: 0;
min-width: 0;
--ha-split-panel-min: 10%;
--ha-split-panel-max: 80%;
--ha-split-panel-divider-hit-area: var(--ha-space-4);
}
.graph {
border-right: 1px solid var(--divider-color);
max-width: 50%;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
min-height: 0;
}
hat-script-graph {
flex: 1;
@@ -699,8 +642,6 @@ export class HaAutomationTrace extends LitElement {
}
.info {
flex: 1;
min-width: 0;
min-height: 0;
overflow-y: auto;
background-color: var(--card-background-color);
}
@@ -454,11 +454,9 @@ export const cleanupRemovedGeneratedTriggerReferences = (
};
/**
* Assign a fresh generated ID to every referenced leaf sharing a stored ID.
* Assign a fresh generated ID to every leaf sharing a stored ID, including manual IDs.
* Expand trigger-condition references to all replacements for the old ID, preserving
* their original "any of these triggers" meaning. Duplicate IDs that no trigger
* condition references are stripped instead, so the fix does not create generated
* IDs nobody uses. Return the original config if IDs are unique.
* their original "any of these triggers" meaning. Return the original config if IDs are unique.
* Templates and action data that inspect trigger.id directly are not rewritten;
* the controller's confirmation dialog warns about that limitation.
*/
@@ -472,21 +470,6 @@ export const makeDuplicateTriggerIdsUnique = (
return config;
}
const referencedIds = new Set<string>();
new AutomationTriggerConditionMapper((condition) =>
mapReferencedTriggerIds(condition, (id) => {
referencedIds.add(id);
return id;
})
).map(config);
const referencedDuplicates = new Set(
[...duplicates].filter((id) => referencedIds.has(id))
);
const unreferencedDuplicates = new Set(
[...duplicates].filter((id) => !referencedIds.has(id))
);
const reservedIds = new Set(ids.filter((id) => !duplicates.has(id)));
const generatedIds = new Set<string>();
const assignments = new Map<Trigger, string>();
@@ -496,7 +479,7 @@ export const makeDuplicateTriggerIdsUnique = (
// trigger its own ID, references to the old ID must expand to every new ID.
flattenTriggers(config.triggers).forEach((trigger) => {
const id = getTriggerId(trigger);
if (!id || !referencedDuplicates.has(id)) {
if (!id || !duplicates.has(id)) {
return;
}
const generatedId = getGeneratedTriggerId(reservedIds, generatedIds);
@@ -504,32 +487,14 @@ export const makeDuplicateTriggerIdsUnique = (
replacementIds.set(id, [...(replacementIds.get(id) || []), generatedId]);
});
const triggers = walkLeafTriggers(config.triggers, (trigger) => {
if (assignments.has(trigger)) {
return { ...trigger, id: assignments.get(trigger) };
}
if (isTriggerList(trigger)) {
return trigger;
}
const id = getTriggerId(trigger);
if (id && unreferencedDuplicates.has(id)) {
const { id: _id, ...rest } = trigger;
return rest as Trigger;
}
return trigger;
}) as Trigger | Trigger[];
if (!referencedDuplicates.size) {
return {
...config,
triggers,
};
}
return {
...new AutomationTriggerConditionMapper((condition) =>
mapReferencedTriggerIds(condition, (id) => replacementIds.get(id) ?? id)
).map(config),
triggers,
triggers: walkLeafTriggers(config.triggers, (trigger) =>
assignments.has(trigger)
? { ...trigger, id: assignments.get(trigger) }
: trigger
) as Trigger | Trigger[],
};
};
@@ -4,6 +4,7 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-check-list-item";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import "../../../../../components/ha-icon-button";
@@ -8,6 +8,7 @@ import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/item/ha-row-item";
import { downloadEmergencyKit } from "../../../../data/backup";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
@@ -34,6 +34,7 @@ import "../../../components/ha-dropdown-item";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon";
import "../../../components/ha-icon-next";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import type {
@@ -11,6 +11,7 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon";
import "../../../components/ha-icon-next";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import type {
@@ -28,6 +29,7 @@ import {
import type { ManagerStateEvent } from "../../../data/backup_manager";
import type { CloudStatus } from "../../../data/cloud";
import "../../../layouts/hass-subpage";
import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import { showAlertDialog } from "../../lovelace/custom-card-helpers";
@@ -22,6 +22,7 @@ import type { CloudStatus } from "../../../data/cloud";
import { subscribeConfigEntries } from "../../../data/config_entries";
import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast";
@@ -27,6 +27,7 @@ import type {
RowClickedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-button";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
@@ -6,6 +6,7 @@ import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-markdown-element";
import "../../../../components/ha-dialog";
import "../../../../components/ha-select";
import "../../../../components/ha-spinner";
import { fetchSupportPackage } from "../../../../data/cloud";
import type { HomeAssistant } from "../../../../types";
@@ -43,9 +43,11 @@ import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-check-list-item";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels";
@@ -27,6 +27,7 @@ import "../../../helpers/forms/ha-input_select-form";
import "../../../helpers/forms/ha-input_text-form";
import "../../../helpers/forms/ha-schedule-form";
import "../../../helpers/forms/ha-timer-form";
import "../../../voice-assistants/entity-voice-settings";
import "../../entity-registry-settings-editor";
import type { EntityRegistrySettingsEditor } from "../../entity-registry-settings-editor";
import { getDeleteConfirmationText } from "../../get-delete-confirmation-text";
@@ -25,6 +25,7 @@ import "../../../components/ha-area-picker";
import "../../../components/ha-color-picker";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button-next";
import "../../../components/ha-icon-picker";
import "../../../components/ha-labels-picker";
import "../../../components/ha-select";
@@ -54,6 +54,7 @@ import type {
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-check-list-item";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
@@ -48,6 +48,7 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-filter-categories";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
@@ -20,6 +20,7 @@ import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-dialog";
import "../../../components/ha-domain-icon";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
import "../../../components/ha-svg-icon";
@@ -11,7 +11,10 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import {
stopKeydownEnterSpacePropagation,
stopPropagation,
} from "../../../common/dom/stop_propagation";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { getDeviceArea } from "../../../common/entity/context/get_device_context";
import { navigate } from "../../../common/navigate";
@@ -74,8 +77,7 @@ class HaConfigEntryDeviceRow extends LitElement {
area ? area.name : undefined,
].filter(Boolean);
return html`<ha-md-list-item
type="button"
return html`<ha-list-item-button
@click=${this._handleNavigateToDevice}
class=${classMap({ disabled: Boolean(device.disabled_by) })}
>
@@ -125,15 +127,14 @@ class HaConfigEntryDeviceRow extends LitElement {
: nothing
}</span
>
${
!this.narrow ? html`<ha-icon-next slot="end"> </ha-icon-next>` : nothing
}
${!this.narrow ? html`<ha-icon-next slot="end"></ha-icon-next>` : nothing}
<div class="vertical-divider" slot="end" @click=${stopPropagation}></div>
${
!this.narrow
? html`<ha-icon-button
slot="end"
@click=${this._handleEditDeviceButton}
@keydown=${stopKeydownEnterSpacePropagation}
.path=${mdiPencil}
.label=${this.hass.localize(
"ui.panel.config.integrations.config_entry.device.edit"
@@ -145,6 +146,7 @@ class HaConfigEntryDeviceRow extends LitElement {
<ha-dropdown
slot="end"
@click=${stopPropagation}
@keydown=${stopKeydownEnterSpacePropagation}
@wa-select=${this._handleMenuAction}
>
<ha-icon-button
@@ -221,7 +223,7 @@ class HaConfigEntryDeviceRow extends LitElement {
: nothing
}
</ha-dropdown>
</ha-md-list-item> `;
</ha-list-item-button>`;
}
private _getEntities = (): EntityRegistryEntry[] =>
@@ -376,25 +378,26 @@ class HaConfigEntryDeviceRow extends LitElement {
static styles = [
haStyle,
css`
:host {
ha-list-item-button {
border-top: 1px solid var(--divider-color);
--ha-row-item-padding-inline: 56px 16px;
}
ha-md-list-item {
--md-list-item-leading-space: 56px;
--md-ripple-hover-color: transparent;
--md-ripple-pressed-color: transparent;
ha-icon-button,
ha-icon-next,
ha-svg-icon {
color: var(--ha-color-fill-neutral-loud-resting);
}
:host([is-child]) ha-md-list-item {
--md-list-item-leading-space: 88px;
:host([is-child]) ha-list-item-button {
--ha-row-item-padding-inline: 88px 16px;
}
.disabled {
opacity: 0.5;
}
:host([narrow]) ha-md-list-item {
--md-list-item-leading-space: 16px;
:host([narrow]) ha-list-item-button {
--ha-row-item-padding-inline: 16px;
}
:host([narrow][is-child]) ha-md-list-item {
--md-list-item-leading-space: 48px;
:host([narrow][is-child]) ha-list-item-button {
--ha-row-item-padding-inline: 48px 16px;
}
ha-tree-indicator {
width: 48px;
@@ -405,8 +408,9 @@ class HaConfigEntryDeviceRow extends LitElement {
width: 1px;
background: var(--divider-color);
}
a {
text-decoration: none;
ha-list-item-button::part(end) {
align-self: stretch;
gap: var(--ha-space-4);
}
`,
];
@@ -28,6 +28,9 @@ import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/item/ha-list-item-button";
import "../../../components/item/ha-row-item";
import "../../../components/list/ha-list-base";
import {
deleteApplicationCredential,
fetchApplicationCredentialsConfigEntry,
@@ -43,9 +46,9 @@ import {
reloadConfigEntry,
updateConfigEntry,
} from "../../../data/config_entries";
import { groupDevicesByParent } from "../../../data/device/device_registry";
import type { DiagnosticInfo } from "../../../data/diagnostics";
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics";
import { groupDevicesByParent } from "../../../data/device/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import type { IntegrationManifest } from "../../../data/integration";
import {
@@ -164,8 +167,8 @@ export class HaConfigEntryRow extends LitElement {
const subEntries = this.data.subEntries;
return html`<ha-md-list>
<ha-md-list-item
return html` <div class="config-entry-wrapper">
<ha-row-item
class=${classMap({
config_entry: true,
"state-not-loaded": item!.state === "not_loaded",
@@ -245,89 +248,82 @@ export class HaConfigEntryRow extends LitElement {
${
allDevices.length
? html`
<a
<ha-dropdown-item
href=${
allDevices.length === 1
? `/config/devices/device/${allDevices[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`
}
value="devices"
>
<ha-dropdown-item value="devices">
<ha-svg-icon
.path=${mdiDevices}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.devices`,
{ count: allDevices.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
</a>
<ha-svg-icon .path=${mdiDevices} slot="icon"></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.devices`,
{ count: allDevices.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
`
: nothing
}
${
allServices.length
? html`
<a
<ha-dropdown-item
href=${
allServices.length === 1
? `/config/devices/device/${allServices[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`
}
value="services"
>
<ha-dropdown-item value="services">
<ha-svg-icon
.path=${mdiHandExtendedOutline}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.services`,
{ count: allServices.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
</a>
<ha-svg-icon
.path=${mdiHandExtendedOutline}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.services`,
{ count: allServices.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
`
: nothing
}
${
entities.length
? html`
<a
<ha-dropdown-item
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
value="entities"
>
<ha-dropdown-item value="entities">
<ha-svg-icon
.path=${mdiShapeOutline}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.entities`,
{ count: entities.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
</a>
<ha-svg-icon
.path=${mdiShapeOutline}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.entities`,
{ count: entities.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
`
: nothing
}
${
ERROR_STATES.includes(item.state)
? html`
<a
<ha-dropdown-item
href=${`/config/logs?filter=${encodeURIComponent(item.domain)}`}
value="logs"
>
<ha-dropdown-item value="logs">
<ha-svg-icon
slot="icon"
.path=${mdiTextBoxOutline}
></ha-svg-icon>
${this.hass.localize("ui.panel.config.logs.caption")}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
</a>
<ha-svg-icon
slot="icon"
.path=${mdiTextBoxOutline}
></ha-svg-icon>
${this.hass.localize("ui.panel.config.logs.caption")}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
`
: nothing
}
@@ -376,22 +372,18 @@ export class HaConfigEntryRow extends LitElement {
${
this.diagnosticHandler && item.state === "loaded"
? html`
<a
<ha-dropdown-item
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
target="_blank"
rel="noreferrer"
@click=${this._signUrl}
value="diagnostics"
>
<ha-dropdown-item value="diagnostics">
<ha-svg-icon
slot="icon"
.path=${mdiDownload}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.download_diagnostics"
)}
</ha-dropdown-item>
</a>
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.download_diagnostics"
)}
</ha-dropdown-item>
`
: nothing
}
@@ -452,82 +444,85 @@ export class HaConfigEntryRow extends LitElement {
: nothing
}
</ha-dropdown>
</ha-md-list-item>
${
this._expanded
? subEntries.length
? html`${
ownDevices.length
? html`<ha-md-list class="devices">
<ha-md-list-item
@click=${this._toggleOwnDevices}
type="button"
class="toggle-devices-row ${classMap({
expanded: this._devicesExpanded,
})}"
>
<ha-icon-button
class="expand-button ${classMap({
</ha-row-item>
<ha-list-base>
${
this._expanded
? subEntries.length
? html`${
ownDevices.length
? html`<div class="devices">
<ha-list-item-button
@click=${this._toggleOwnDevices}
class="toggle-devices-row ${classMap({
expanded: this._devicesExpanded,
})}"
.path=${mdiChevronDown}
slot="start"
>
</ha-icon-button>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.devices_without_subentry"
)}
</ha-md-list-item>
${
this._devicesExpanded
? groupDevicesByParent(ownDevices).map(
({ device, isChild, isLastChild }) =>
html`<ha-config-entry-device-row
.hass=${this.hass}
.narrow=${this.narrow}
.entry=${item}
.device=${device}
.entities=${entities}
.isChild=${isChild}
.isLastChild=${isLastChild}
></ha-config-entry-device-row>`
)
: nothing
}
</ha-md-list>`
: nothing
}
${subEntries.map(
(subEntryData) => html`
<ha-config-sub-entry-row
.hass=${this.hass}
.narrow=${this.narrow}
.manifest=${this.manifest}
.diagnosticHandler=${this.diagnosticHandler}
.entities=${this.entities}
.entry=${item}
.data=${subEntryData}
data-entry-id=${item.entry_id}
></ha-config-sub-entry-row>
`
)}`
: html`
${groupDevicesByParent(ownDevices).map(
({ device, isChild, isLastChild }) =>
html`<ha-config-entry-device-row
<ha-icon-button
class="expand-button ${classMap({
expanded: this._devicesExpanded,
})}"
.path=${mdiChevronDown}
slot="start"
>
</ha-icon-button>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.devices_without_subentry"
)}</span
>
</ha-list-item-button>
${
this._devicesExpanded
? groupDevicesByParent(ownDevices).map(
({ device, isChild, isLastChild }) =>
html`<ha-config-entry-device-row
.hass=${this.hass}
.narrow=${this.narrow}
.entry=${item}
.device=${device}
.entities=${entities}
.isChild=${isChild}
.isLastChild=${isLastChild}
></ha-config-entry-device-row>`
)
: nothing
}
</div>`
: nothing
}
${subEntries.map(
(subEntryData) => html`
<ha-config-sub-entry-row
.hass=${this.hass}
.narrow=${this.narrow}
.manifest=${this.manifest}
.diagnosticHandler=${this.diagnosticHandler}
.entities=${this.entities}
.entry=${item}
.device=${device}
.entities=${entities}
.isChild=${isChild}
.isLastChild=${isLastChild}
></ha-config-entry-device-row>`
)}
`
: nothing
}
</ha-md-list>`;
.data=${subEntryData}
data-entry-id=${item.entry_id}
></ha-config-sub-entry-row>
`
)}`
: html`
${groupDevicesByParent(ownDevices).map(
({ device, isChild, isLastChild }) =>
html`<ha-config-entry-device-row
.hass=${this.hass}
.narrow=${this.narrow}
.entry=${item}
.device=${device}
.entities=${entities}
.isChild=${isChild}
.isLastChild=${isLastChild}
></ha-config-entry-device-row>`
)}
`
: nothing
}
</ha-list-base>
</div>`;
}
private _configPanel = memoizeOne(getConfigPanelPath);
@@ -849,18 +844,22 @@ export class HaConfigEntryRow extends LitElement {
.expand-button.expanded {
transform: rotate(180deg);
}
ha-md-list {
.config-entry-wrapper {
border: 1px solid var(--divider-color);
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
padding: 0;
background-color: var(--card-background-color);
}
:host([narrow]) {
margin-left: -12px;
margin-right: -12px;
}
ha-md-list.devices {
.devices {
margin: 16px;
margin-top: 0;
background-color: var(--card-background-color);
}
ha-icon-button {
color: var(--ha-color-fill-neutral-loud-resting);
}
ha-icon-button.link {
color: var(
@@ -879,6 +878,11 @@ export class HaConfigEntryRow extends LitElement {
ha-dropdown a {
text-decoration: none;
}
.message {
display: flex;
align-items: center;
gap: var(--ha-space-2);
}
`,
];
}
@@ -30,10 +30,10 @@ import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../components/input/ha-input-search";
import "../../../components/item/ha-list-item-base";
import "../../../components/list/ha-list-base";
import { getSignedPath } from "../../../data/auth";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
import {
@@ -68,6 +68,7 @@ import { QUALITY_SCALE_MAP } from "../../../data/integration_quality_scale";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { multiTermSearch } from "../../../resources/fuseMultiTerm";
@@ -754,11 +755,11 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
"ui.panel.config.integrations.discovered"
)}
</h3>
<ha-md-list class="discovered">
<ha-list-base class="discovered">
${filteredDiscoveryData.map(
(flow) =>
html`<ha-md-list-item class="discovered">
${flow.localized_title}
html`<ha-list-item-base class="discovered">
<span slot="headline">${flow.localized_title}</span>
<ha-button
slot="end"
variant="success"
@@ -768,9 +769,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
>
${this.hass.localize("ui.common.add")}
</ha-button>
</ha-md-list-item>`
</ha-list-item-base>`
)}
</ha-md-list>
</ha-list-base>
</div>
`
: nothing
@@ -786,15 +787,17 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
</h3>
${
filteredAttentionFlows.length
? html`<ha-md-list class="attention">
? html`<ha-list-base class="attention">
${filteredAttentionFlows.map((flow) => {
const attention = ATTENTION_SOURCES.includes(
flow.context.source
);
return html`<ha-md-list-item
return html`<ha-list-item-base
class="config_entry ${attention ? "attention" : ""}"
>
${flow.localized_title}
<span slot="headline"
>${flow.localized_title}</span
>
<span slot="supporting-text"
>${this.hass.localize(
`ui.panel.config.integrations.${
@@ -813,9 +816,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}`
)}</ha-button
>
</ha-md-list-item>`;
</ha-list-item-base>`;
})}
</ha-md-list>`
</ha-list-base>`
: nothing
}
${filteredAttentionData.map(
@@ -1515,26 +1518,21 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
color: var(--mdc-theme-text-icon-on-background, rgba(0, 0, 0, 0.38));
animation: unset;
}
ha-md-list {
ha-list-base {
border: 1px solid var(--divider-color);
border-radius: var(--ha-border-radius-md);
padding: 0;
overflow: hidden;
}
.discovered {
--md-list-container-color: rgba(var(--rgb-success-color), 0.2);
ha-list-base.discovered {
background-color: rgba(var(--rgb-success-color), 0.2);
}
.attention {
--md-list-container-color: rgba(var(--rgb-warning-color), 0.2);
ha-list-base.attention {
background-color: rgba(var(--rgb-warning-color), 0.2);
}
ha-md-list-item {
--md-list-item-top-space: 4px;
--md-list-item-bottom-space: 4px;
position: relative;
ha-list-item-base.discovered {
--ha-row-item-min-height: 72px;
}
ha-md-list-item.discovered {
height: 72px;
}
ha-md-list-item.config_entry::after {
ha-list-item-base.config_entry::after {
position: absolute;
top: 0;
right: 0;
@@ -1596,7 +1594,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
.state-disabled [slot="supporting-text"] {
opacity: var(--md-list-item-disabled-opacity, 0.3);
}
ha-md-list {
ha-list-base {
margin-top: 8px;
margin-bottom: 8px;
}
@@ -10,6 +10,7 @@ import {
} from "../../../data/config_flow";
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
import { domainToName } from "../../../data/integration";
import "../../../layouts/hass-loading-screen";
import { ChildPanelReady } from "../../../layouts/panel-ready";
import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page";
@@ -13,13 +13,14 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/item/ha-row-item";
import "../../../components/list/ha-list-base";
import type { ConfigEntry } from "../../../data/config_entries";
import { deleteSubEntry, updateSubEntry } from "../../../data/config_entries";
import { groupDevicesByParent } from "../../../data/device/device_registry";
import type { DiagnosticInfo } from "../../../data/diagnostics";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import type { IntegrationManifest } from "../../../data/integration";
import type { SubEntryData } from "./ha-config-integration-page";
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
import type { HomeAssistant } from "../../../types";
import {
@@ -27,6 +28,7 @@ import {
showPromptDialog,
} from "../../lovelace/custom-card-helpers";
import "./ha-config-entry-device-row";
import type { SubEntryData } from "./ha-config-integration-page";
@customElement("ha-config-sub-entry-row")
class HaConfigSubEntryRow extends LitElement {
@@ -54,8 +56,8 @@ class HaConfigSubEntryRow extends LitElement {
const services = this.data.services;
const entities = this._getEntities();
return html`<ha-md-list>
<ha-md-list-item
return html`<div class="sub-entry-card">
<ha-row-item
class="sub-entry"
data-entry-id=${configEntry.entry_id}
.configEntry=${configEntry}
@@ -107,71 +109,65 @@ class HaConfigSubEntryRow extends LitElement {
${
devices.length || services.length
? html`
<a
<ha-dropdown-item
href=${
devices.length === 1
? `/config/devices/device/${devices[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}`
}
value="devices"
>
<ha-dropdown-item value="devices">
<ha-svg-icon
.path=${mdiDevices}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.devices`,
{ count: devices.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
</a>
<ha-svg-icon .path=${mdiDevices} slot="icon"></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.devices`,
{ count: devices.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
`
: nothing
}
${
services.length
? html`
<a
<ha-dropdown-item
href=${
services.length === 1
? `/config/devices/device/${services[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}`
}
value="services"
>
<ha-dropdown-item value="services">
<ha-svg-icon
.path=${mdiHandExtendedOutline}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.services`,
{ count: services.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
</a>
<ha-svg-icon
.path=${mdiHandExtendedOutline}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.services`,
{ count: services.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
`
: nothing
}
${
entities.length
? html`
<a
<ha-dropdown-item
href=${`/config/entities?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}`}
value="entities"
>
<ha-dropdown-item value="entities">
<ha-svg-icon
.path=${mdiShapeOutline}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.entities`,
{ count: entities.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
</a>
<ha-svg-icon
.path=${mdiShapeOutline}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.entities`,
{ count: entities.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
`
: nothing
}
@@ -188,36 +184,38 @@ class HaConfigSubEntryRow extends LitElement {
)}
</ha-dropdown-item>
</ha-dropdown>
</ha-md-list-item>
${
this._expanded
? html`
${groupDevicesByParent(devices).map(
({ device, isChild, isLastChild }) =>
html`<ha-config-entry-device-row
.hass=${this.hass}
.narrow=${this.narrow}
.entry=${this.entry}
.device=${device}
.entities=${this.entities}
.isChild=${isChild}
.isLastChild=${isLastChild}
></ha-config-entry-device-row>`
)}
${services.map(
(service) =>
html`<ha-config-entry-device-row
.hass=${this.hass}
.narrow=${this.narrow}
.entry=${this.entry}
.device=${service}
.entities=${this.entities}
></ha-config-entry-device-row>`
)}
`
: nothing
}
</ha-md-list>`;
</ha-row-item>
<ha-list-base>
${
this._expanded
? html`
${groupDevicesByParent(devices).map(
({ device, isChild, isLastChild }) =>
html`<ha-config-entry-device-row
.hass=${this.hass}
.narrow=${this.narrow}
.entry=${this.entry}
.device=${device}
.entities=${this.entities}
.isChild=${isChild}
.isLastChild=${isLastChild}
></ha-config-entry-device-row>`
)}
${services.map(
(service) =>
html`<ha-config-entry-device-row
.hass=${this.hass}
.narrow=${this.narrow}
.entry=${this.entry}
.device=${service}
.entities=${this.entities}
></ha-config-entry-device-row>`
)}
`
: nothing
}
</ha-list-base>
</div>`;
}
private _toggleExpand() {
@@ -305,18 +303,19 @@ class HaConfigSubEntryRow extends LitElement {
.expand-button.expanded {
transform: rotate(180deg);
}
ha-md-list {
.sub-entry-card {
border: 1px solid var(--divider-color);
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
padding: 0;
margin: 16px;
margin-top: 0;
}
ha-md-list-item.has-subentries {
border-bottom: 1px solid var(--divider-color);
ha-icon-button,
ha-icon-next,
ha-svg-icon {
color: var(--ha-color-fill-neutral-loud-resting);
}
ha-dropdown a {
text-decoration: none;
ha-row-item.has-subentries {
border-bottom: 1px solid var(--divider-color);
}
`;
}
@@ -9,6 +9,8 @@ import { extractSearchParamsObject } from "../../../../../common/url/search-para
import type { DataTableColumnContainer } from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-metric";
import "../../../../../components/ha-relative-time";
import type {
BluetoothAllocationsData,
BluetoothConnectionData,
@@ -174,6 +174,11 @@ class ZHANetworkInfoPage extends LitElement {
max-width: 600px;
margin: auto;
}
ha-list-item-base::part(supporting-text) {
font-size: var(--ha-font-size-m);
white-space: normal;
}
`,
];
}
@@ -111,6 +111,11 @@ class ZWaveJSNetworkInfoPage extends LitElement {
max-width: 600px;
margin: auto;
}
ha-list-item-base::part(supporting-text) {
font-size: var(--ha-font-size-m);
white-space: normal;
}
`,
];
}
@@ -31,6 +31,7 @@ import { computeRTL } from "../../../common/util/compute_rtl";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/device/ha-device-picker";
import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
+95 -154
View File
@@ -13,12 +13,8 @@ import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import {
fireEvent,
type HASSDomTargetEvent,
} from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -49,14 +45,9 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import "../../../components/ha-trace-picker";
import "../../../components/ha-split-panel";
import type { HaSplitPanel } from "../../../components/ha-split-panel";
const TABS = ["details", "timeline", "logbook", "config"] as const;
const STORAGE_KEY_SPLIT_POSITION = "script-trace-split-position";
const DEFAULT_SPLIT_POSITION = 20;
@customElement("ha-script-trace")
export class HaScriptTrace extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -89,8 +80,6 @@ export class HaScriptTrace extends LitElement {
@state() private _view: (typeof TABS)[number] | "blueprint" = "details";
@state() private _splitPosition = DEFAULT_SPLIT_POSITION;
@query("hat-script-graph") private _graph?: HatScriptGraph;
protected render(): TemplateResult {
@@ -98,6 +87,10 @@ export class HaScriptTrace extends LitElement {
? this.hass.states[this._entityId]
: undefined;
const graph = this._graph;
const trackedNodes = graph?.trackedNodes;
const renderedNodes = graph?.renderedNodes;
const title = stateObj?.attributes.friendly_name || this._entityId;
let devButtons: TemplateResult | string = "";
@@ -227,19 +220,96 @@ export class HaScriptTrace extends LitElement {
? ""
: html`
<div class="main">
${
this.narrow
? this._renderPanes()
: html`
<ha-split-panel
class="split"
.position=${this._splitPosition}
@wa-reposition=${this._splitRepositioned}
<div class="graph">
<hat-script-graph
.trace=${this._trace}
.selected=${this._selected?.path}
@graph-node-selected=${this._pickNode}
></hat-script-graph>
</div>
<div class="info">
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
${TABS.map(
(view) => html`
<ha-tab-group-tab
slot="nav"
.active=${this._view === view}
.panel=${view}
>
${this._renderPanes()}
</ha-split-panel>
${this.hass.localize(
`ui.panel.config.automation.trace.tabs.${
view === "config" ? "script_config" : view
}`
)}
</ha-tab-group-tab>
`
}
)}
${
this._trace.blueprint_inputs
? html`
<ha-tab-group-tab
slot="nav"
.active=${this._view === "blueprint"}
panel="blueprint"
>
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.blueprint_config`
)}
</ha-tab-group-tab>
`
: ""
}
</ha-tab-group>
${
this._selected === undefined ||
this._logbookEntries === undefined ||
trackedNodes === undefined
? ""
: this._view === "details"
? html`
<ha-trace-path-details
.hass=${this.hass}
.narrow=${this.narrow}
.trace=${this._trace}
.selected=${this._selected}
.logbookEntries=${this._logbookEntries}
.trackedNodes=${trackedNodes}
.renderedNodes=${renderedNodes!}
></ha-trace-path-details>
`
: this._view === "config"
? html`
<ha-trace-config
.trace=${this._trace}
></ha-trace-config>
`
: this._view === "logbook"
? html`
<ha-trace-logbook
.hass=${this.hass}
.narrow=${this.narrow}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries}
></ha-trace-logbook>
`
: this._view === "blueprint"
? html`
<ha-trace-blueprint-config
.trace=${this._trace}
></ha-trace-blueprint-config>
`
: html`
<ha-trace-timeline
.hass=${this.hass}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries}
.selected=${this._selected}
@value-changed=${this._timelinePathPicked}
></ha-trace-timeline>
`
}
</div>
</div>
`
}
@@ -247,114 +317,9 @@ export class HaScriptTrace extends LitElement {
`;
}
private _renderPanes(): TemplateResult {
const graph = this._graph;
const trackedNodes = graph?.trackedNodes;
const renderedNodes = graph?.renderedNodes;
return html`
<div class="graph" slot="start">
<hat-script-graph
.trace=${this._trace}
.selected=${this._selected?.path}
@graph-node-selected=${this._pickNode}
></hat-script-graph>
</div>
<div class="info" slot="end">
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
${TABS.map(
(view) => html`
<ha-tab-group-tab
slot="nav"
.active=${this._view === view}
.panel=${view}
>
${this.hass.localize(
`ui.panel.config.automation.trace.tabs.${
view === "config" ? "script_config" : view
}`
)}
</ha-tab-group-tab>
`
)}
${
this._trace!.blueprint_inputs
? html`
<ha-tab-group-tab
slot="nav"
.active=${this._view === "blueprint"}
panel="blueprint"
>
${this.hass!.localize(
`ui.panel.config.automation.trace.tabs.blueprint_config`
)}
</ha-tab-group-tab>
`
: ""
}
</ha-tab-group>
${
this._selected === undefined ||
this._logbookEntries === undefined ||
trackedNodes === undefined
? ""
: this._view === "details"
? html`
<ha-trace-path-details
.hass=${this.hass}
.narrow=${this.narrow}
.trace=${this._trace}
.selected=${this._selected}
.logbookEntries=${this._logbookEntries}
.trackedNodes=${trackedNodes}
.renderedNodes=${renderedNodes!}
></ha-trace-path-details>
`
: this._view === "config"
? html`
<ha-trace-config .trace=${this._trace}></ha-trace-config>
`
: this._view === "logbook"
? html`
<ha-trace-logbook
.hass=${this.hass}
.narrow=${this.narrow}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries}
></ha-trace-logbook>
`
: this._view === "blueprint"
? html`
<ha-trace-blueprint-config
.trace=${this._trace}
></ha-trace-blueprint-config>
`
: html`
<ha-trace-timeline
.hass=${this.hass}
.trace=${this._trace}
.logbookEntries=${this._logbookEntries}
.selected=${this._selected}
@value-changed=${this._timelinePathPicked}
></ha-trace-timeline>
`
}
</div>
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
const storedPosition = localStorage?.[STORAGE_KEY_SPLIT_POSITION];
if (storedPosition) {
const parsed = parseFloat(storedPosition);
if (!isNaN(parsed) && parsed > 0 && parsed < 100) {
this._splitPosition = parsed;
}
}
if (!this.scriptId) {
return;
}
@@ -444,19 +409,6 @@ export class HaScriptTrace extends LitElement {
this._selected = ev.detail;
}
private _splitRepositioned(ev: HASSDomTargetEvent<HaSplitPanel>) {
this._splitPosition = ev.target.position;
this._storeSplitPosition();
}
private _storeSplitPosition = debounce(
() => {
localStorage[STORAGE_KEY_SPLIT_POSITION] = String(this._splitPosition);
},
500,
false
);
private _refreshTraces() {
this._loadTraces();
}
@@ -642,22 +594,13 @@ export class HaScriptTrace extends LitElement {
padding: 16px;
}
ha-split-panel.split {
flex: 1;
min-height: 0;
min-width: 0;
--ha-split-panel-min: 10%;
--ha-split-panel-max: 80%;
--ha-split-panel-divider-hit-area: var(--ha-space-4);
}
.graph {
border-right: 1px solid var(--divider-color);
max-width: 50%;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
min-height: 0;
}
hat-script-graph {
flex: 1;
@@ -675,8 +618,6 @@ export class HaScriptTrace extends LitElement {
}
.info {
flex: 1;
min-width: 0;
min-height: 0;
overflow-y: auto;
background-color: var(--card-background-color);
}
@@ -46,6 +46,7 @@ import "../../../layouts/hass-subpage";
import { panelIsReady } from "../../../layouts/panel-ready";
import type { HomeAssistant, Route } from "../../../types";
import { bytesToString } from "../../../util/bytes-to-string";
import "../core/ha-config-analytics";
import { showMoveDatadiskDialog } from "./show-dialog-move-datadisk";
import { showMountViewDialog } from "./show-dialog-view-mount";
import "./storage-breakdown-chart";
@@ -25,6 +25,7 @@ import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-input-helper-text";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tip";
import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import "../../../../components/input/ha-input";
@@ -31,6 +31,7 @@ import { DialogMixin } from "../../../dialogs/dialog-mixin";
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
import { haStyle, haStyleScrollbar } from "../../../resources/styles";
import { loadVirtualizer } from "../../../resources/virtualizer";
import "./entity-voice-settings";
import type { ExposeEntityDialogParams } from "./show-dialog-expose-entity";
@customElement("dialog-expose-entity")
@@ -24,6 +24,7 @@ import "./assist-pipeline-detail/assist-pipeline-detail-conversation";
import "./assist-pipeline-detail/assist-pipeline-detail-stt";
import "./assist-pipeline-detail/assist-pipeline-detail-tts";
import "./assist-pipeline-detail/assist-pipeline-detail-wakeword";
import "./debug/assist-render-pipeline-events";
import type { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-voice-assistant-pipeline-detail";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -58,6 +58,7 @@ import {
getAssistantsTableColumn,
} from "./expose/assistants-table-column";
import { getAvailableAssistants } from "./expose/available-assistants";
import "./expose/expose-assistant-icon";
import { voiceAssistantTabs } from "./ha-config-voice-assistants";
import { showExposeEntityDialog } from "./show-dialog-expose-entity";
import { showVoiceSettingsDialog } from "./show-dialog-voice-settings";
+3
View File
@@ -6,6 +6,7 @@ import { sanitizeNavigationPath } from "../../common/url/sanitize-navigation-pat
import "../../components/ha-alert";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import "../../components/ha-top-app-bar-fixed";
import type { EnergyFrontendSystemData } from "../../data/frontend";
import { fetchFrontendSystemData } from "../../data/frontend";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
@@ -14,6 +15,8 @@ import type { HomeAssistant, PanelInfo } from "../../types";
import { generateLovelaceDashboardStrategy } from "../lovelace/strategies/get-strategy";
import "../lovelace/hui-root";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import { DEFAULT_POWER_COLLECTION_KEY } from "../../data/energy";
@customElement("ha-panel-energy")
@@ -12,6 +12,7 @@ import { computeStateDomain } from "../../../common/entity/compute_state_domain"
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-badge";
import "../../../components/ha-ripple";
import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
import { cameraUrlWithWidthHeight } from "../../../data/camera";
@@ -1,4 +1,5 @@
import { customElement } from "lit/decorators";
import "../../../components/entity/ha-state-label-badge";
import type { HuiStateLabelBadgeEditor } from "../editor/config-elements/hui-state-label-badge-editor";
import { HuiEntityBadge } from "./hui-entity-badge";
import type { EntityBadgeConfig, StateLabelBadgeConfig } from "./types";
@@ -7,6 +7,7 @@ import { consumeEntityState } from "../../../common/decorators/consume-context-e
import { transform } from "../../../common/decorators/transform";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-number-buttons";
import "../../../components/ha-control-slider";
import "../../../components/ha-icon";
@@ -1,73 +0,0 @@
/**
* Home-circle stroke lengths for the energy distribution card.
*
* Hourly allocation can produce used_solar + used_battery + used_grid larger
* than net used_total when some hours export more than they produce. Dividing
* by net used_total then overflows the circle and the leftover grid arc goes
* negative, which browsers paint as a solid grid ring.
*
* These arcs use the sum of the allocated home flows as the denominator so
* they always fit the circumference.
*/
export const ENERGY_DISTRIBUTION_HOME_CIRCLE_CIRCUMFERENCE = 238.76104;
export interface EnergyDistributionHomeCircleArcs {
solar?: number;
battery?: number;
lowCarbon?: number;
highCarbon?: number;
grid?: number;
}
export const computeEnergyDistributionHomeCircleArcs = ({
usedSolar = 0,
usedBattery = 0,
usedGrid = 0,
hasSolar,
hasGrid,
highCarbonConsumption,
circumference = ENERGY_DISTRIBUTION_HOME_CIRCLE_CIRCUMFERENCE,
}: {
usedSolar?: number;
usedBattery?: number;
usedGrid?: number;
hasSolar: boolean;
hasGrid: boolean;
highCarbonConsumption?: number;
circumference?: number;
}): EnergyDistributionHomeCircleArcs => {
const solar = Math.max(usedSolar, 0);
const battery = Math.max(usedBattery, 0);
const grid = Math.max(usedGrid, 0);
const ringTotal = solar + battery + grid;
const arcs: EnergyDistributionHomeCircleArcs = {};
// Leave arcs unset so the card can fall back to the plain home border
// instead of painting zero-length dashes over a borderless circle.
if (ringTotal <= 0) {
return arcs;
}
const share = (value: number): number => circumference * (value / ringTotal);
if (hasSolar) {
arcs.solar = share(solar);
}
if (battery > 0) {
arcs.battery = share(battery);
}
if (hasGrid) {
if (highCarbonConsumption !== undefined) {
const highCarbon = Math.min(Math.max(highCarbonConsumption, 0), grid);
arcs.highCarbon = share(highCarbon);
// Keep 0 defined: the card mounts the home SVG when solar or
// lowCarbon is defined, not when the low-carbon stroke is painted.
arcs.lowCarbon = share(grid - highCarbon);
arcs.grid = arcs.highCarbon;
} else {
arcs.grid = share(grid);
}
}
return arcs;
};
@@ -37,10 +37,8 @@ import type { LovelaceCard } from "../../types";
import type { EnergyDistributionCardConfig } from "../types";
import { formatNumber } from "../../../../common/number/format_number";
import { round } from "../../../../common/number/round";
import {
ENERGY_DISTRIBUTION_HOME_CIRCLE_CIRCUMFERENCE as CIRCLE_CIRCUMFERENCE,
computeEnergyDistributionHomeCircleArcs,
} from "./energy-distribution-home-circle";
const CIRCLE_CIRCUMFERENCE = 238.76104;
// Flows are differences of sums; anything that rounds to 0 Wh is noise
const hasFlow = (value: number | null): value is number =>
@@ -323,8 +321,22 @@ class HuiEnergyDistrubutionCard
const totalHomeConsumption = Math.max(0, consumption.total.used_total);
let homeSolarCircumference: number | undefined;
if (hasSolarProduction) {
homeSolarCircumference =
CIRCLE_CIRCUMFERENCE * (solarConsumption! / totalHomeConsumption);
}
let homeBatteryCircumference: number | undefined;
if (batteryConsumption) {
homeBatteryCircumference =
CIRCLE_CIRCUMFERENCE * (batteryConsumption / totalHomeConsumption);
}
let lowCarbonEnergy: number | undefined;
let highCarbonConsumption: number | undefined;
let homeLowCarbonCircumference: number | undefined;
let homeHighCarbonCircumference: number | undefined;
// This fallback is used in the demo
let electricityMapUrl = "https://app.electricitymaps.com";
@@ -348,6 +360,7 @@ class HuiEnergyDistrubutionCard
if (highCarbonEnergy !== null) {
lowCarbonEnergy = totalFromGrid - highCarbonEnergy;
let highCarbonConsumption: number;
if (gridConsumption !== totalFromGrid) {
// Only get the part that was used for consumption and not the battery
highCarbonConsumption =
@@ -355,23 +368,18 @@ class HuiEnergyDistrubutionCard
} else {
highCarbonConsumption = highCarbonEnergy;
}
homeHighCarbonCircumference =
CIRCLE_CIRCUMFERENCE * (highCarbonConsumption / totalHomeConsumption);
homeLowCarbonCircumference =
CIRCLE_CIRCUMFERENCE -
(homeSolarCircumference || 0) -
(homeBatteryCircumference || 0) -
homeHighCarbonCircumference;
}
}
const {
solar: homeSolarCircumference,
battery: homeBatteryCircumference,
lowCarbon: homeLowCarbonCircumference,
grid: homeGridCircumference,
} = computeEnergyDistributionHomeCircleArcs({
usedSolar: solarConsumption ?? 0,
usedBattery: batteryConsumption ?? 0,
usedGrid: gridConsumption,
hasSolar: hasSolarProduction,
hasGrid: Boolean(hasGrid),
highCarbonConsumption,
});
const totalLines =
gridConsumption +
(solarConsumption || 0) +
@@ -662,8 +670,16 @@ class HuiEnergyDistrubutionCard
cx="40"
cy="40"
r="38"
stroke-dasharray="${homeGridCircumference} ${
CIRCLE_CIRCUMFERENCE - (homeGridCircumference ?? 0)
stroke-dasharray="${
homeHighCarbonCircumference ??
CIRCLE_CIRCUMFERENCE -
homeSolarCircumference! -
(homeBatteryCircumference || 0)
} ${
homeHighCarbonCircumference !== undefined
? CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference
: homeSolarCircumference! +
(homeBatteryCircumference || 0)
}"
stroke-dashoffset="0"
shape-rendering="geometricPrecision"
@@ -25,6 +25,8 @@ import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
import "../../../components/ha-aspect-ratio";
import "../../../components/ha-card";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-domain-icon";
import "../../../components/ha-icon";
import "../../../components/tile/ha-tile-badge";
import "../../../components/tile/ha-tile-container";
@@ -40,6 +40,7 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-list";
import "../../../components/ha-markdown-element";
import "../../../components/ha-relative-time";
import "../../../components/ha-select";
import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import "../../../components/input/ha-input";
@@ -3,6 +3,7 @@ import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { refine } from "superstruct";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-assist-pipeline-picker";
import type {
HaFormSchema,
SchemaUnion,
@@ -11,6 +11,7 @@ import "../../../../components/entity/state-badge";
import "../../../../components/ha-button";
import "../../../../components/ha-combo-box-item";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-ripple";
import "../../../../components/ha-section-title";
import "../../../../components/ha-svg-icon";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
@@ -33,6 +33,7 @@ import type { HomeAssistant } from "../../../../types";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
import "../../badges/hui-badge";
import { getConfigEntityId } from "../../common/get-config-entity-id";
import "../../sections/hui-section";
import { addBadge, replaceBadge } from "../config-util";
import { getBadgeDefaultConfig } from "../get-badge-default-config";
import { getBadgeDocumentationURL } from "../get-dashboard-documentation-url";
@@ -12,6 +12,7 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-grid-size-picker";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-slider";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-switch";
import "../../../../components/ha-yaml-editor";
@@ -11,6 +11,7 @@ import "../../../../components/entity/state-badge";
import "../../../../components/ha-button";
import "../../../../components/ha-combo-box-item";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-ripple";
import "../../../../components/ha-section-title";
import "../../../../components/ha-svg-icon";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
@@ -2,6 +2,7 @@ import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { array, assert, literal, object, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-check-list-item";
import "../../../../../components/ha-switch";
import "../../../../../components/ha-list";
import type { HomeAssistant } from "../../../../../types";
@@ -34,6 +34,7 @@ import { baseLovelaceBadgeConfig } from "../structs/base-badge-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style";
import { stateContentHasTimestamp } from "../../../../state-display/state-display";
import "./hui-card-features-editor";
import { timeFormatConfigStruct } from "../../components/types";
const badgeConfigStruct = assign(
@@ -13,6 +13,7 @@ import {
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entities-picker";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import "../../../../components/ha-target-picker";
@@ -28,6 +28,7 @@ import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import "../../../../components/ha-formfield";
import "../../../../components/ha-selector/ha-selector-select";
import "../../../../components/ha-switch";
import { MAP_CARD_MARKER_LABEL_MODES } from "../../../../components/map/ha-map";
@@ -5,6 +5,7 @@ import { assert, assign, object, optional, string, union } from "superstruct";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import "../../../../components/ha-theme-picker";
import type { HomeAssistant } from "../../../../types";
import type { PictureCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
@@ -42,6 +42,7 @@ import {
haStyleDialogFixedTop,
} from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../../components/hui-entity-editor";
import type { Lovelace } from "../../types";
import { SECTIONS_VIEW_LAYOUT } from "../../views/const";
import { generateDefaultSection } from "../../views/default-section";
@@ -1,6 +1,7 @@
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-humidifier-state";
import type { HumidifierEntity } from "../../../data/humidifier";
import type { HomeAssistant } from "../../../types";
@@ -1,6 +1,7 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-button";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { activateScene } from "../../../data/scene";
@@ -1,6 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import { setTimeValue } from "../../../data/time";
@@ -3,7 +3,6 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import "../../../components/ha-state-icon";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { ForecastEvent, WeatherEntity } from "../../../data/weather";
@@ -21,6 +20,7 @@ import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction, hasAnyAction } from "../common/has-action";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceRow } from "./types";
import "../../../state-display/state-display";
@@ -1,5 +1,6 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-label-badge";
import "../../../components/ha-svg-icon";
import type { LovelaceSectionElement } from "../../../data/lovelace";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
@@ -4,6 +4,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/entity/ha-state-label-badge";
import "../../../components/ha-button";
import "../../../components/ha-svg-icon";
import type { LovelaceViewElement } from "../../../data/lovelace";
@@ -21,6 +21,7 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import type { HuiBadge } from "../badges/hui-badge";
import type { HuiCard } from "../cards/hui-card";
import "../components/hui-badge-edit-mode";
import "../components/hui-section-edit-mode";
import { addSection, moveCard, moveSection } from "../editor/config-util";
import type { LovelaceCardPath } from "../editor/lovelace-path";
@@ -16,6 +16,7 @@ import type { HomeAssistant } from "../../../types";
import type { HuiBadge } from "../badges/hui-badge";
import "../badges/hui-view-badges";
import type { HuiCard } from "../cards/hui-card";
import "../components/hui-badge-edit-mode";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { replaceView } from "../editor/config-util";
import { showEditViewHeaderDialog } from "../editor/view-header/show-edit-view-header-dialog";
+1
View File
@@ -7,6 +7,7 @@ import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { debounce } from "../../../common/util/debounce";
import { deepEqual } from "../../../common/util/deep-equal";
import "../../../components/entity/ha-state-label-badge";
import "../../../components/ha-svg-icon";
import type { LovelaceViewElement } from "../../../data/lovelace";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
@@ -3,7 +3,6 @@ import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import "../../components/ha-card";
import "../../components/ha-md-list";
import { isExternal } from "../../data/external";
import "../../layouts/hass-subpage";
import { haStyle } from "../../resources/styles";
@@ -46,47 +45,42 @@ class HaProfileSectionBrowser extends LitElement {
<div class="card-content">
${this.hass.localize("ui.panel.profile.client_settings_detail")}
</div>
<ha-md-list>
${
this.hass.dockedSidebar !== "auto" || !this.narrow
? html`
<ha-force-narrow-row
.hass=${this.hass}
></ha-force-narrow-row>
`
: ""
}
${
"vibrate" in navigator
? html`
<ha-set-vibrate-row
.hass=${this.hass}
></ha-set-vibrate-row>
`
: ""
}
${
!isExternal &&
isComponentLoaded(this.hass.config, "html5.notify")
? html`
<ha-push-notifications-row
.hass=${this.hass}
></ha-push-notifications-row>
`
: ""
}
<ha-set-suspend-row .hass=${this.hass}></ha-set-suspend-row>
${
!isMobileClient
? html`
<ha-enable-shortcuts-row
id="shortcuts"
.hass=${this.hass}
></ha-enable-shortcuts-row>
`
: ""
}
</ha-md-list>
${
this.hass.dockedSidebar !== "auto" || !this.narrow
? html`
<ha-force-narrow-row
.hass=${this.hass}
></ha-force-narrow-row>
`
: ""
}
${
"vibrate" in navigator
? html`
<ha-set-vibrate-row .hass=${this.hass}></ha-set-vibrate-row>
`
: ""
}
${
!isExternal && isComponentLoaded(this.hass.config, "html5.notify")
? html`
<ha-push-notifications-row
.hass=${this.hass}
></ha-push-notifications-row>
`
: ""
}
<ha-set-suspend-row .hass=${this.hass}></ha-set-suspend-row>
${
!isMobileClient
? html`
<ha-enable-shortcuts-row
id="shortcuts"
.hass=${this.hass}
></ha-enable-shortcuts-row>
`
: ""
}
</ha-card>
</div>
</hass-subpage>
@@ -110,12 +104,6 @@ class HaProfileSectionBrowser extends LitElement {
margin: 0 auto var(--ha-space-4);
max-width: 600px;
}
ha-md-list {
background: none;
padding-top: 0;
padding-bottom: 0;
}
`,
];
}
@@ -4,8 +4,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-button";
import "../../components/ha-card";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/item/ha-row-item";
import type { CoreFrontendUserData } from "../../data/frontend";
import { subscribeFrontendUserData } from "../../data/frontend";
import { showEditSidebarDialog } from "../../dialogs/sidebar/show-dialog-edit-sidebar";
@@ -81,40 +80,38 @@ class HaProfileSectionPreferences extends LitElement {
.narrow=${this.narrow}
.hass=${this.hass}
></ha-pick-dashboard-row>
<ha-md-list>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.profile.customize_sidebar.header"
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.profile.customize_sidebar.description"
)}</span
>
<ha-button
slot="end"
appearance="plain"
size="s"
@click=${this._customizeSidebar}
>
${this.hass.localize(
"ui.panel.profile.customize_sidebar.button"
)}
</ha-button>
</ha-md-list-item>
${
this.hass.user!.is_admin
? html`
<ha-entity-id-picker-row
.hass=${this.hass}
.coreUserData=${this._coreUserData}
></ha-entity-id-picker-row>
`
: ""
}
</ha-md-list>
<ha-row-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.profile.customize_sidebar.header"
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.profile.customize_sidebar.description"
)}</span
>
<ha-button
slot="end"
appearance="plain"
size="s"
@click=${this._customizeSidebar}
>
${this.hass.localize(
"ui.panel.profile.customize_sidebar.button"
)}
</ha-button>
</ha-row-item>
${
this.hass.user!.is_admin
? html`
<ha-entity-id-picker-row
.hass=${this.hass}
.coreUserData=${this._coreUserData}
></ha-entity-id-picker-row>
`
: ""
}
</ha-card>
</div>
</hass-subpage>
@@ -142,12 +139,6 @@ class HaProfileSectionPreferences extends LitElement {
margin: 0 auto var(--ha-space-4);
max-width: 600px;
}
ha-md-list {
background: none;
padding-top: 0;
padding-bottom: 0;
}
`,
];
}
+1
View File
@@ -2,6 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../components/entity/ha-entity-toggle";
import "../components/entity/state-info";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
+1
View File
@@ -2,6 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../components/entity/ha-entity-toggle";
import "../components/entity/state-info";
import "../components/ha-button";
import { UNAVAILABLE } from "../data/entity/entity";
+3 -48
View File
@@ -18,10 +18,7 @@ import {
subscribeFrontendUserData,
} from "../data/frontend";
import { forwardHaptic } from "../data/haptics";
import {
getServiceCallEntityIds,
serviceCallWillDisconnect,
} from "../data/service";
import { serviceCallWillDisconnect } from "../data/service";
import {
DateFormat,
FirstWeekday,
@@ -35,12 +32,7 @@ import { preserveUnchangedRecord } from "../common/util/preserve-unchanged-recor
import { subscribeFloorRegistry } from "../data/ws-floor_registry";
import { subscribePanels } from "../data/ws-panels";
import { translationMetadata } from "../resources/translations-metadata";
import type {
Constructor,
HomeAssistant,
ServiceCallRequest,
ServiceCallResponse,
} from "../types";
import type { Constructor, HomeAssistant, ServiceCallResponse } from "../types";
import {
addBrandsAuth,
clearBrandsTokenRefresh,
@@ -122,7 +114,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
);
}
try {
const response = (await callService(
return (await callService(
conn,
domain,
service,
@@ -130,24 +122,11 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
target,
returnResponse
)) as ServiceCallResponse;
this._reportEntityControlToExternalApp(
domain,
service,
serviceData,
target
);
return response;
} catch (err: any) {
if (
err.error?.code === ERR_CONNECTION_LOST &&
serviceCallWillDisconnect(domain, service, serviceData)
) {
this._reportEntityControlToExternalApp(
domain,
service,
serviceData,
target
);
return { context: { id: "" } };
}
if (this.hass?.debugConnection) {
@@ -426,28 +405,4 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
this._updateHass({});
}
}
private _reportEntityControlToExternalApp(
domain: string,
service: string,
serviceData?: ServiceCallRequest["serviceData"],
target?: ServiceCallRequest["target"]
) {
const external = this.hass?.auth.external;
if (!external) {
return;
}
const entityIds = getServiceCallEntityIds(serviceData, target);
if (!entityIds.length) {
return;
}
try {
external.fireMessage({
type: "entity/controlled",
payload: { entity_ids: entityIds, domain, service },
});
} catch (_err) {
// Reporting is best effort and must not fail the service call.
}
}
};
+2 -2
View File
@@ -5866,8 +5866,8 @@
"missing_triggers": "This condition references a trigger that no longer exists. Uncheck it to clear it.",
"duplicate_ids": "Some triggers share the same trigger ID. Avoid reusing a trigger ID to group triggers and select multiple triggers here instead.",
"duplicate_ids_fix": "Fix",
"assign_unique_ids_title": "Fix duplicate trigger IDs?",
"assign_unique_ids_description": "Triggers that share a used ID get a unique ID, and conditions using it are updated to match. IDs that nothing uses are removed. Templates or actions that use trigger.id directly may need to be updated.",
"assign_unique_ids_title": "Assign unique trigger IDs?",
"assign_unique_ids_description": "Each trigger sharing an ID gets a unique one, and conditions using it are updated to match. Templates or actions that use trigger.id directly may need to be updated.",
"description": {
"picker": "Tests if the automation has been triggered by a specific trigger.",
"full": "If triggered by {id}",
@@ -1,92 +0,0 @@
import { describe, expect, it } from "vitest";
import type { StatisticsMetaData } from "../../src/data/recorder";
import { getStatisticLabel, StatisticMeanType } from "../../src/data/recorder";
import {
mockArea,
mockDevice,
mockEntity,
} from "../common/entity/context/context-mock";
import { createMockEntityState, createMockHass } from "../fixtures/hass";
const metadata = (
statistic_id: string,
name: string | null
): StatisticsMetaData => ({
statistic_id,
name,
source: "recorder",
statistics_unit_of_measurement: "kWh",
has_sum: true,
mean_type: StatisticMeanType.NONE,
unit_class: "energy",
});
// The friendly name deliberately differs from the composed name, so the
// assertions fail if the label is taken from the attribute again.
const hassWithRegistry = () =>
createMockHass(
{
"sensor.dishwasher_energy": createMockEntityState(
"sensor.dishwasher_energy",
"12",
{ friendly_name: "Smart plug 3 energy" }
),
},
{
entities: {
"sensor.dishwasher_energy": mockEntity({
entity_id: "sensor.dishwasher_energy",
device_id: "device_1",
name: "Energy",
}),
},
devices: {
device_1: mockDevice({
id: "device_1",
name: "Dishwasher",
area_id: "kitchen",
}),
},
areas: { kitchen: mockArea({ area_id: "kitchen", name: "Kitchen" }) },
}
);
describe("getStatisticLabel", () => {
it("composes the name from the registry rather than friendly_name", () => {
expect(
getStatisticLabel(
hassWithRegistry(),
"sensor.dishwasher_energy",
metadata("sensor.dishwasher_energy", "Recorder name")
)
).toBe("Dishwasher Energy");
});
// External statistics have no entity, so there is no registry context to
// compose a name from and the recorder metadata is all we have.
it("uses the metadata name when there is no entity", () => {
expect(
getStatisticLabel(
createMockHass(),
"energy:solar_production",
metadata("energy:solar_production", "Solar production")
)
).toBe("Solar production");
});
it("falls back to the statistic id when metadata has no name", () => {
expect(
getStatisticLabel(
createMockHass(),
"energy:solar_production",
metadata("energy:solar_production", null)
)
).toBe("energy:solar_production");
});
it("falls back to the statistic id when there is no metadata", () => {
expect(
getStatisticLabel(createMockHass(), "energy:solar_production", undefined)
).toBe("energy:solar_production");
});
});
-50
View File
@@ -1,50 +0,0 @@
import { describe, expect, it } from "vitest";
import { getServiceCallEntityIds } from "../../src/data/service";
describe("getServiceCallEntityIds", () => {
it("returns an empty list when no entities are targeted", () => {
expect(getServiceCallEntityIds()).toEqual([]);
expect(getServiceCallEntityIds({ brightness: 50 }, {})).toEqual([]);
expect(getServiceCallEntityIds({}, { area_id: "kitchen" })).toEqual([]);
});
it("reads a single entity from target or service data", () => {
expect(getServiceCallEntityIds({}, { entity_id: "light.a" })).toEqual([
"light.a",
]);
expect(getServiceCallEntityIds({ entity_id: "light.a" })).toEqual([
"light.a",
]);
});
it("prefers the target over the legacy service data entity ids", () => {
expect(
getServiceCallEntityIds(
{ entity_id: ["light.a", "light.b"] },
{ entity_id: ["light.b", "light.c"] }
)
).toEqual(["light.b", "light.c"]);
});
it("deduplicates entity ids", () => {
expect(
getServiceCallEntityIds({}, { entity_id: ["light.a", "light.a"] })
).toEqual(["light.a"]);
});
it("splits comma separated ids and lowercases them like Core does", () => {
expect(
getServiceCallEntityIds({}, { entity_id: "Light.A, light.b ,light.a" })
).toEqual(["light.a", "light.b"]);
});
it("ignores wildcard, malformed, and non-string entity ids", () => {
expect(getServiceCallEntityIds({ entity_id: "all" })).toEqual([]);
expect(
getServiceCallEntityIds(
{},
{ entity_id: ["all", "none", "", "light", 5] as unknown as string[] }
)
).toEqual([]);
});
});
@@ -290,25 +290,6 @@ describe("automation trigger IDs", () => {
]);
});
it("strips unreferenced duplicate IDs instead of generating new ones", () => {
const config: AutomationConfig = {
triggers: [
{ trigger: "state", entity_id: "light.kitchen", id: "motion" },
{ trigger: "time", at: "12:00:00", id: "motion" },
],
conditions: [{ condition: "trigger", id: "" }],
actions: [],
};
const updated = makeDuplicateTriggerIdsUnique(config);
expect(updated.triggers).toEqual([
{ trigger: "state", entity_id: "light.kitchen" },
{ trigger: "time", at: "12:00:00" },
]);
expect(updated.conditions).toBe(config.conditions);
});
it("removes generated trigger IDs that no condition or action references", () => {
const generatedA = `${GENERATED_TRIGGER_ID_PREFIX}aB3x`;
const generatedB = `${GENERATED_TRIGGER_ID_PREFIX}yZ7w`;
@@ -1,119 +0,0 @@
/**
* Protects the energy-distribution home ring from overflowing when hourly
* allocation yields more used_solar/used_battery/used_grid than net used_total
* (https://github.com/home-assistant/frontend/issues/54185).
*/
import { assert, describe, it } from "vitest";
import {
ENERGY_DISTRIBUTION_HOME_CIRCLE_CIRCUMFERENCE as CIRCLE,
computeEnergyDistributionHomeCircleArcs,
} from "../../../../../src/panels/lovelace/cards/energy/energy-distribution-home-circle";
const sumDefinedArcs = (
arcs: ReturnType<typeof computeEnergyDistributionHomeCircleArcs>
): number =>
(arcs.solar ?? 0) +
(arcs.battery ?? 0) +
(arcs.lowCarbon ?? 0) +
(arcs.grid ?? 0);
describe("computeEnergyDistributionHomeCircleArcs", () => {
it("sizes arcs from allocated home flows so they fill the circle", () => {
const arcs = computeEnergyDistributionHomeCircleArcs({
usedSolar: 8.5,
usedBattery: 4.6,
usedGrid: 0.12,
hasSolar: true,
hasGrid: true,
});
assert.approximately(arcs.solar!, CIRCLE * (8.5 / 13.22), 1e-6);
assert.approximately(arcs.battery!, CIRCLE * (4.6 / 13.22), 1e-6);
assert.approximately(arcs.grid!, CIRCLE * (0.12 / 13.22), 1e-6);
assert.approximately(sumDefinedArcs(arcs), CIRCLE, 1e-6);
assert.isAtLeast(arcs.grid!, 0);
});
it("does not paint a full grid ring when used_solar exceeds net home energy", () => {
// Screenshot totals from #54185 with solar and export in different hours:
// used_solar 77.1, used_battery 0, used_grid 0, net used_total 13.22.
const arcs = computeEnergyDistributionHomeCircleArcs({
usedSolar: 77.1,
usedBattery: 0,
usedGrid: 0,
hasSolar: true,
hasGrid: true,
});
assert.approximately(arcs.solar!, CIRCLE, 1e-6);
assert.isUndefined(arcs.battery);
assert.equal(arcs.grid, 0);
assert.approximately(sumDefinedArcs(arcs), CIRCLE, 1e-6);
});
it("keeps a zero-consumption ring from producing NaN or negative dashes", () => {
const arcs = computeEnergyDistributionHomeCircleArcs({
usedSolar: 0,
usedBattery: 0,
usedGrid: 0,
hasSolar: true,
hasGrid: true,
});
assert.deepEqual(arcs, {});
});
it("splits grid into low-carbon and high-carbon without exceeding the grid share", () => {
const arcs = computeEnergyDistributionHomeCircleArcs({
usedSolar: 4,
usedBattery: 0,
usedGrid: 6,
hasSolar: true,
hasGrid: true,
highCarbonConsumption: 2,
});
const ringTotal = 10;
assert.approximately(arcs.solar!, CIRCLE * (4 / ringTotal), 1e-6);
assert.approximately(arcs.lowCarbon!, CIRCLE * (4 / ringTotal), 1e-6);
assert.approximately(arcs.grid!, CIRCLE * (2 / ringTotal), 1e-6);
assert.approximately(sumDefinedArcs(arcs), CIRCLE, 1e-6);
assert.isAtLeast(arcs.lowCarbon!, 0);
assert.isAtLeast(arcs.grid!, 0);
});
it("clamps high-carbon consumption to the grid share", () => {
const arcs = computeEnergyDistributionHomeCircleArcs({
usedSolar: 5,
usedBattery: 0,
usedGrid: 5,
hasSolar: true,
hasGrid: true,
highCarbonConsumption: 50,
});
assert.equal(arcs.lowCarbon, 0);
assert.approximately(arcs.grid!, CIRCLE * 0.5, 1e-6);
assert.approximately(sumDefinedArcs(arcs), CIRCLE, 1e-6);
});
it("keeps a defined zero low-carbon arc so a grid-only high-carbon ring still renders", () => {
// The card only mounts the home SVG when solar or lowCarbon is defined.
// Omitting 0 would drop battery/grid arcs in a no-solar 100% fossil grid.
const arcs = computeEnergyDistributionHomeCircleArcs({
usedSolar: 0,
usedBattery: 4,
usedGrid: 6,
hasSolar: false,
hasGrid: true,
highCarbonConsumption: 6,
});
assert.isUndefined(arcs.solar);
assert.equal(arcs.lowCarbon, 0);
assert.approximately(arcs.battery!, CIRCLE * 0.4, 1e-6);
assert.approximately(arcs.grid!, CIRCLE * 0.6, 1e-6);
assert.approximately(sumDefinedArcs(arcs), CIRCLE, 1e-6);
});
});