Compare commits

..

8 Commits

49 changed files with 1654 additions and 1798 deletions

2
.nvmrc
View File

@@ -1 +1 @@
24.11.1
22.21.1

View File

@@ -260,6 +260,7 @@ const createRspackConfig = ({
),
},
experiments: {
layers: true,
outputModule: true,
},
};

View File

@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor";
import type { LegacyTrigger } from "../../../../src/data/automation";
import type { Trigger } from "../../../../src/data/automation";
import { describeTrigger } from "../../../../src/data/automation_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
@@ -66,7 +66,7 @@ const triggers = [
},
];
const initialTrigger: LegacyTrigger = {
const initialTrigger: Trigger = {
trigger: "state",
entity_id: "light.kitchen",
};

View File

@@ -237,6 +237,6 @@
},
"packageManager": "yarn@4.10.3",
"volta": {
"node": "24.11.1"
"node": "22.21.1"
}
}

View File

@@ -1,30 +0,0 @@
/**
* Executes a callback within a View Transition if supported, otherwise runs it directly.
*
* @param callback - Function to execute. Can be synchronous or return a Promise. The callback will be passed a boolean indicating whether the view transition is available.
* @returns Promise that resolves when the transition completes (or immediately if not supported)
*
* @example
* ```typescript
* // Synchronous callback
* withViewTransition(() => {
* this.large = !this.large;
* });
*
* // Async callback
* await withViewTransition(async () => {
* await this.updateData();
* });
* ```
*/
export const withViewTransition = (
callback: (viewTransitionAvailable: boolean) => void | Promise<void>
): Promise<void> => {
if (document.startViewTransition) {
return document.startViewTransition(() => callback(true)).finished;
}
// Fallback: Execute callback directly without transition
const result = callback(false);
return result instanceof Promise ? result : Promise.resolve();
};

View File

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

View File

@@ -0,0 +1,28 @@
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-section-title")
class HaSectionTitle extends LitElement {
protected render() {
return html`<slot></slot>`;
}
static styles = css`
:host {
display: block;
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-section-title": HaSectionTitle;
}
}

View File

@@ -1,8 +1,6 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import type { StateSelector } from "../../data/selector";
import { extractFromTarget } from "../../data/target";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import "../entity/ha-entity-state-picker";
@@ -27,29 +25,15 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public context?: {
filter_attribute?: string;
filter_entity?: string | string[];
filter_target?: HassServiceTarget;
};
@state() private _entityIds?: string | string[];
willUpdate(changedProps) {
if (changedProps.has("selector") || changedProps.has("context")) {
this._resolveEntityIds(
this.selector.state?.entity_id,
this.context?.filter_entity,
this.context?.filter_target
).then((entityIds) => {
this._entityIds = entityIds;
});
}
}
protected render() {
if (this.selector.state?.multiple) {
return html`
<ha-entity-states-picker
.hass=${this.hass}
.entityId=${this._entityIds}
.entityId=${this.selector.state?.entity_id ||
this.context?.filter_entity}
.attribute=${this.selector.state?.attribute ||
this.context?.filter_attribute}
.extraOptions=${this.selector.state?.extra_options}
@@ -66,7 +50,8 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
return html`
<ha-entity-state-picker
.hass=${this.hass}
.entityId=${this._entityIds}
.entityId=${this.selector.state?.entity_id ||
this.context?.filter_entity}
.attribute=${this.selector.state?.attribute ||
this.context?.filter_attribute}
.extraOptions=${this.selector.state?.extra_options}
@@ -80,24 +65,6 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
></ha-entity-state-picker>
`;
}
private async _resolveEntityIds(
selectorEntityId: string | string[] | undefined,
contextFilterEntity: string | string[] | undefined,
contextFilterTarget: HassServiceTarget | undefined
): Promise<string | string[] | undefined> {
if (selectorEntityId !== undefined) {
return selectorEntityId;
}
if (contextFilterEntity !== undefined) {
return contextFilterEntity;
}
if (contextFilterTarget !== undefined) {
const result = await extractFromTarget(this.hass, contextFilterTarget);
return result.referenced_entities;
}
return undefined;
}
}
declare global {

View File

@@ -858,7 +858,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (!filterType || filterType === "label") {
let labels = this._getLabelsMemoized(
this.hass,
this.hass.states,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this._labelRegistry,
includeDomains,
undefined,

View File

@@ -1,97 +0,0 @@
import {
mdiAvTimer,
mdiCalendar,
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiHomeAssistant,
mdiMapMarker,
mdiMapMarkerRadius,
mdiMessageAlert,
mdiMicrophoneMessage,
mdiNfcVariant,
mdiNumeric,
mdiStateMachine,
mdiSwapHorizontal,
mdiWeatherSunny,
mdiWebhook,
} from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { computeDomain } from "../common/entity/compute_domain";
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
export const TRIGGER_ICONS = {
calendar: mdiCalendar,
device: mdiDevices,
event: mdiGestureDoubleTap,
state: mdiStateMachine,
geo_location: mdiMapMarker,
homeassistant: mdiHomeAssistant,
mqtt: mdiSwapHorizontal,
numeric_state: mdiNumeric,
sun: mdiWeatherSunny,
conversation: mdiMicrophoneMessage,
tag: mdiNfcVariant,
template: mdiCodeBraces,
time: mdiClockOutline,
time_pattern: mdiAvTimer,
webhook: mdiWebhook,
persistent_notification: mdiMessageAlert,
zone: mdiMapMarkerRadius,
list: mdiFormatListBulleted,
};
@customElement("ha-trigger-icon")
export class HaTriggerIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public trigger?: string;
@property() public icon?: string;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
}
if (!this.trigger) {
return nothing;
}
if (!this.hass) {
return this._renderFallback();
}
const icon = triggerIcon(this.hass, this.trigger).then((icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
return this._renderFallback();
});
return html`${until(icon)}`;
}
private _renderFallback() {
const domain = computeDomain(this.trigger!);
return html`
<ha-svg-icon
.path=${TRIGGER_ICONS[this.trigger!] || FALLBACK_DOMAIN_ICONS[domain]}
></ha-svg-icon>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-trigger-icon": HaTriggerIcon;
}
}

View File

@@ -50,7 +50,7 @@ export const ACTION_COLLECTIONS: AutomationElementGroupCollection[] = [
{
groups: {
device_id: {},
dynamicGroups: {},
serviceGroups: {},
},
},
{
@@ -117,6 +117,14 @@ export const VIRTUAL_ACTIONS: Partial<
},
} as const;
export const SERVICE_PREFIX = "__SERVICE__";
export const isService = (key: string | undefined): boolean | undefined =>
key?.startsWith(SERVICE_PREFIX);
export const getService = (key: string): string =>
key.substring(SERVICE_PREFIX.length);
export const COLLAPSIBLE_ACTION_ELEMENTS = [
"ha-automation-action-choose",
"ha-automation-action-condition",

View File

@@ -24,11 +24,54 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
area?: AreaRegistryEntry;
}
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
floor?: FloorRegistryEntry;
areas: FloorComboBoxItem[];
}
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
areas: FloorComboBoxItem[];
}
export interface AreaFloorValue {
id: string;
type: "floor" | "area";
}
export const getAreasNestedInFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
formatId: (value: AreaFloorValue) => string,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[],
includeEmptyFloors = false
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors,
includeEmptyFloors,
true
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
export const getAreasAndFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
@@ -42,8 +85,47 @@ export const getAreasAndFloors = (
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[]
): FloorComboBoxItem[] => {
excludeFloors?: string[],
includeEmptyFloors = false
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors,
includeEmptyFloors
) as FloorComboBoxItem[];
const getAreasAndFloorsItems = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
formatId: (value: AreaFloorValue) => string,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[],
includeEmptyFloors = false,
nested = false
): (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] => {
const floors = Object.values(haFloors);
const areas = Object.values(haAreas);
const devices = Object.values(haDevices);
@@ -189,6 +271,14 @@ export const getAreasAndFloors = (
const compare = floorCompare(haFloors);
if (includeEmptyFloors) {
Object.values(haFloors).forEach((floor) => {
if (!floorAreaLookup[floor.floor_id]) {
floorAreaLookup[floor.floor_id] = [];
}
});
}
// @ts-ignore
const floorAreaEntries: [
FloorRegistryEntry | undefined,
@@ -200,9 +290,15 @@ export const getAreasAndFloors = (
})
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
const items: FloorComboBoxItem[] = [];
const items: (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] = [];
floorAreaEntries.forEach(([floor, floorAreas]) => {
let floorItem: FloorComboBoxItem | FloorNestedComboBoxItem;
if (floor) {
const floorName = computeFloorName(floor);
@@ -213,7 +309,7 @@ export const getAreasAndFloors = (
})
.flat();
items.push({
floorItem = {
id: formatId({ id: floor.floor_id, type: "floor" }),
type: "floor",
primary: floorName,
@@ -225,10 +321,9 @@ export const getAreasAndFloors = (
...floor.aliases,
...areaSearchLabels,
],
});
};
}
items.push(
...floorAreas.map((area) => {
const floorAreasItems = floorAreas.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
@@ -238,12 +333,20 @@ export const getAreasAndFloors = (
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
})
);
});
items.push(
...unassignedAreas.map((area) => {
if (floor) {
items.push(floorItem!);
}
if (nested && floor) {
(floorItem! as FloorNestedComboBoxItem).areas = floorAreasItems;
} else {
items.push(...floorAreasItems);
}
});
const unassignedAreaItems = unassignedAreas.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
@@ -253,8 +356,15 @@ export const getAreasAndFloors = (
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
})
);
});
if (nested && unassignedAreaItems.length) {
items.push({
areas: unassignedAreaItems,
} as UnassignedAreasFloorComboBoxItem);
} else {
items.push(...unassignedAreaItems);
}
return items;
};

View File

@@ -60,11 +60,12 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
});
export const getAreaEntityLookup = (
entities: EntityRegistryEntry[]
entities: EntityRegistryEntry[],
filterHidden = false
): AreaEntityLookup => {
const areaEntityLookup: AreaEntityLookup = {};
for (const entity of entities) {
if (!entity.area_id) {
if (!entity.area_id || (filterHidden && entity.hidden_by)) {
continue;
}
if (!(entity.area_id in areaEntityLookup)) {

View File

@@ -1,10 +1,8 @@
import type {
HassEntityAttributeBase,
HassEntityBase,
HassServiceTarget,
} from "home-assistant-js-websocket";
import { ensureArray } from "../common/array/ensure-array";
import type { WeekdayShort } from "../common/datetime/weekday";
import { navigate } from "../common/navigate";
import type { LocalizeKeys } from "../common/translations/localize";
import { createSearchParam } from "../common/url/search-params";
@@ -14,19 +12,11 @@ import { CONDITION_BUILDING_BLOCKS } from "./condition";
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
import type { Action, Field, MODES } from "./script";
import { migrateAutomationAction } from "./script";
import type { TriggerDescription } from "./trigger";
import type { WeekdayShort } from "../common/datetime/weekday";
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10;
export const DYNAMIC_PREFIX = "__DYNAMIC__";
export const isDynamic = (key: string | undefined): boolean | undefined =>
key?.startsWith(DYNAMIC_PREFIX);
export const getValueFromDynamic = (key: string): string =>
key.substring(DYNAMIC_PREFIX.length);
export interface AutomationEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & {
id?: string;
@@ -96,12 +86,6 @@ export interface BaseTrigger {
id?: string;
variables?: Record<string, unknown>;
enabled?: boolean;
options?: Record<string, unknown>;
}
export interface PlatformTrigger extends BaseTrigger {
trigger: Exclude<string, LegacyTrigger["trigger"]>;
target?: HassServiceTarget;
}
export interface StateTrigger extends BaseTrigger {
@@ -211,7 +195,7 @@ export interface CalendarTrigger extends BaseTrigger {
offset: string;
}
export type LegacyTrigger =
export type Trigger =
| StateTrigger
| MqttTrigger
| GeoLocationTrigger
@@ -228,9 +212,8 @@ export type LegacyTrigger =
| TemplateTrigger
| EventTrigger
| DeviceTrigger
| CalendarTrigger;
export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger;
| CalendarTrigger
| TriggerList;
interface BaseCondition {
condition: string;
@@ -592,7 +575,6 @@ export interface TriggerSidebarConfig extends BaseSidebarConfig {
insertAfter: (value: Trigger | Trigger[]) => boolean;
toggleYamlMode: () => void;
config: Trigger;
description?: TriggerDescription;
yamlMode: boolean;
uiSupported: boolean;
}

View File

@@ -16,9 +16,8 @@ import {
formatListWithAnds,
formatListWithOrs,
} from "../common/string/format-list";
import { hasTemplate } from "../common/string/has-template";
import type { HomeAssistant } from "../types";
import type { Condition, ForDict, LegacyTrigger, Trigger } from "./automation";
import type { Condition, ForDict, Trigger } from "./automation";
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
import {
localizeDeviceAutomationCondition,
@@ -26,7 +25,8 @@ import {
} from "./device_automation";
import type { EntityRegistryEntry } from "./entity_registry";
import type { FrontendLocaleData } from "./translation";
import { getTriggerDomain, getTriggerObjectId, isTriggerList } from "./trigger";
import { isTriggerList } from "./trigger";
import { hasTemplate } from "../common/string/has-template";
const triggerTranslationBaseKey =
"ui.panel.config.automation.editor.triggers.type";
@@ -121,37 +121,6 @@ const tryDescribeTrigger = (
return trigger.alias;
}
const description = describeLegacyTrigger(
trigger as LegacyTrigger,
hass,
entityRegistry
);
if (description) {
return description;
}
const triggerType = trigger.trigger;
const domain = getTriggerDomain(trigger.trigger);
const type = getTriggerObjectId(trigger.trigger);
return (
hass.localize(
`component.${domain}.triggers.${type}.description_configured`
) ||
hass.localize(
`ui.panel.config.automation.editor.triggers.type.${triggerType as LegacyTrigger["trigger"]}.label`
) ||
hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
);
};
const describeLegacyTrigger = (
trigger: LegacyTrigger,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[]
) => {
// Event Trigger
if (trigger.trigger === "event" && trigger.event_type) {
const eventTypes: string[] = [];
@@ -833,7 +802,13 @@ const describeLegacyTrigger = (
}
);
}
return undefined;
return (
hass.localize(
`ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`
) ||
hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
);
};
export const describeCondition = (

View File

@@ -107,11 +107,12 @@ export const sortDeviceRegistryByName = (
);
export const getDeviceEntityLookup = (
entities: EntityRegistryEntry[]
entities: EntityRegistryEntry[],
filterHidden = false
): DeviceEntityLookup => {
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
if (!entity.device_id || (filterHidden && entity.hidden_by)) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {

View File

@@ -360,35 +360,6 @@ export const getReferencedStatisticIds = (
return statIDs;
};
export const getReferencedStatisticIdsPower = (
prefs: EnergyPreferences
): string[] => {
const statIDs: (string | undefined)[] = [];
for (const source of prefs.energy_sources) {
if (source.type === "gas" || source.type === "water") {
continue;
}
if (source.type === "solar") {
statIDs.push(source.stat_rate);
continue;
}
if (source.type === "battery") {
statIDs.push(source.stat_rate);
continue;
}
if (source.power) {
statIDs.push(...source.power.map((p) => p.stat_rate));
}
}
statIDs.push(...prefs.device_consumption.map((d) => d.stat_rate));
return statIDs.filter(Boolean) as string[];
};
export const enum CompareMode {
NONE = "",
PREVIOUS = "previous",
@@ -436,10 +407,9 @@ const getEnergyData = async (
"gas",
"device",
]);
const powerStatIds = getReferencedStatisticIdsPower(prefs);
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
const allStatIDs = [...energyStatIds, ...waterStatIds];
const dayDifference = differenceInDays(end || new Date(), start);
const period =
@@ -450,8 +420,6 @@ const getEnergyData = async (
: dayDifference > 2
? "day"
: "hour";
const finePeriod =
dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
const statsMetadata: Record<string, StatisticsMetaData> = {};
const statsMetadataArray = allStatIDs.length
@@ -473,9 +441,6 @@ const getEnergyData = async (
? (gasUnit as (typeof VOLUME_UNITS)[number])
: undefined,
};
const powerUnits: StatisticsUnitConfiguration = {
power: "kW",
};
const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata);
const waterUnits: StatisticsUnitConfiguration = {
volume: waterUnit,
@@ -486,12 +451,6 @@ const getEnergyData = async (
"change",
])
: {};
const _powerStats: Statistics | Promise<Statistics> = powerStatIds.length
? fetchStatistics(hass!, start, end, powerStatIds, finePeriod, powerUnits, [
"mean",
])
: {};
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
"change",
@@ -598,7 +557,6 @@ const getEnergyData = async (
const [
energyStats,
powerStats,
waterStats,
energyStatsCompare,
waterStatsCompare,
@@ -606,14 +564,13 @@ const getEnergyData = async (
fossilEnergyConsumptionCompare,
] = await Promise.all([
_energyStats,
_powerStats,
_waterStats,
_energyStatsCompare,
_waterStatsCompare,
_fossilEnergyConsumption,
_fossilEnergyConsumptionCompare,
]);
const stats = { ...energyStats, ...waterStats, ...powerStats };
const stats = { ...energyStats, ...waterStats };
if (compare) {
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
}

View File

@@ -59,7 +59,6 @@ import type {
} from "./entity_registry";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import { getTriggerDomain, getTriggerObjectId } from "./trigger";
/** Icon to use when no icon specified for service. */
export const DEFAULT_SERVICE_ICON = mdiRoomService;
@@ -134,19 +133,14 @@ const resources: {
all?: Promise<Record<string, ServiceIcons>>;
domains: Record<string, ServiceIcons | Promise<ServiceIcons>>;
};
triggers: {
all?: Promise<Record<string, TriggerIcons>>;
domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
};
} = {
entity: {},
entity_component: {},
services: { domains: {} },
triggers: { domains: {} },
};
interface IconResources<
T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons,
T extends ComponentIcons | PlatformIcons | ServiceIcons,
> {
resources: Record<string, T>;
}
@@ -190,22 +184,12 @@ type ServiceIcons = Record<
{ service: string; sections?: Record<string, string> }
>;
type TriggerIcons = Record<
string,
{ trigger: string; sections?: Record<string, string> }
>;
export type IconCategory =
| "entity"
| "entity_component"
| "services"
| "triggers";
export type IconCategory = "entity" | "entity_component" | "services";
interface CategoryType {
entity: PlatformIcons;
entity_component: ComponentIcons;
services: ServiceIcons;
triggers: TriggerIcons;
}
export const getHassIcons = async <T extends IconCategory>(
@@ -274,59 +258,42 @@ export const getComponentIcons = async (
return resources.entity_component.resources.then((res) => res[domain]);
};
export const getCategoryIcons = async <
T extends Exclude<IconCategory, "entity" | "entity_component">,
>(
export const getServiceIcons = async (
hass: HomeAssistant,
category: T,
domain?: string,
force = false
): Promise<CategoryType[T] | Record<string, CategoryType[T]> | undefined> => {
): Promise<ServiceIcons | Record<string, ServiceIcons> | undefined> => {
if (!domain) {
if (!force && resources[category].all) {
return resources[category].all as Promise<
Record<string, CategoryType[T]>
>;
if (!force && resources.services.all) {
return resources.services.all;
}
resources[category].all = getHassIcons(hass, category).then((res) => {
resources[category].domains = res.resources as any;
return res?.resources as Record<string, CategoryType[T]>;
}) as any;
return resources[category].all as Promise<Record<string, CategoryType[T]>>;
resources.services.all = getHassIcons(hass, "services", domain).then(
(res) => {
resources.services.domains = res.resources;
return res?.resources;
}
if (!force && domain in resources[category].domains) {
return resources[category].domains[domain] as Promise<CategoryType[T]>;
);
return resources.services.all;
}
if (resources[category].all && !force) {
await resources[category].all;
if (domain in resources[category].domains) {
return resources[category].domains[domain] as Promise<CategoryType[T]>;
if (!force && domain in resources.services.domains) {
return resources.services.domains[domain];
}
if (resources.services.all && !force) {
await resources.services.all;
if (domain in resources.services.domains) {
return resources.services.domains[domain];
}
}
if (!isComponentLoaded(hass, domain)) {
return undefined;
}
const result = getHassIcons(hass, category, domain);
resources[category].domains[domain] = result.then(
const result = getHassIcons(hass, "services", domain);
resources.services.domains[domain] = result.then(
(res) => res?.resources[domain]
) as any;
return resources[category].domains[domain] as Promise<CategoryType[T]>;
);
return resources.services.domains[domain];
};
export const getServiceIcons = async (
hass: HomeAssistant,
domain?: string,
force = false
): Promise<ServiceIcons | Record<string, ServiceIcons> | undefined> =>
getCategoryIcons(hass, "services", domain, force);
export const getTriggerIcons = async (
hass: HomeAssistant,
domain?: string,
force = false
): Promise<TriggerIcons | Record<string, TriggerIcons> | undefined> =>
getCategoryIcons(hass, "triggers", domain, force);
// Cache for sorted range keys
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
@@ -506,26 +473,6 @@ export const attributeIcon = async (
return icon;
};
export const triggerIcon = async (
hass: HomeAssistant,
trigger: string
): Promise<string | undefined> => {
let icon: string | undefined;
const domain = getTriggerDomain(trigger);
const triggerName = getTriggerObjectId(trigger);
const triggerIcons = await getTriggerIcons(hass, domain);
if (triggerIcons) {
const trgrIcon = triggerIcons[triggerName] as TriggerIcons[string];
icon = trgrIcon?.trigger;
}
if (!icon) {
icon = await domainIcon(hass, domain);
}
return icon;
};
export const serviceIcon = async (
hass: HomeAssistant,
service: string

View File

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

View File

@@ -28,7 +28,6 @@ export interface TodoItem {
status: TodoItemStatus | null;
description?: string | null;
due?: string | null;
completed?: string | null;
}
export const enum TodoListEntityFeature {

View File

@@ -73,8 +73,7 @@ export type TranslationCategory =
| "application_credentials"
| "issues"
| "selector"
| "services"
| "triggers";
| "services";
export const subscribeTranslationPreferences = (
hass: HomeAssistant,

View File

@@ -1,20 +1,57 @@
import { mdiMapClock, mdiShape } from "@mdi/js";
import {
mdiAvTimer,
mdiCalendar,
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiMapClock,
mdiMapMarker,
mdiMapMarkerRadius,
mdiMessageAlert,
mdiMicrophoneMessage,
mdiNfcVariant,
mdiNumeric,
mdiShape,
mdiStateMachine,
mdiSwapHorizontal,
mdiWeatherSunny,
mdiWebhook,
} from "@mdi/js";
import { computeDomain } from "../common/entity/compute_domain";
import { computeObjectId } from "../common/entity/compute_object_id";
import type { HomeAssistant } from "../types";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import type {
AutomationElementGroupCollection,
Trigger,
TriggerList,
} from "./automation";
import type { Selector, TargetSelector } from "./selector";
export const TRIGGER_ICONS = {
calendar: mdiCalendar,
device: mdiDevices,
event: mdiGestureDoubleTap,
state: mdiStateMachine,
geo_location: mdiMapMarker,
homeassistant: mdiHomeAssistant,
mqtt: mdiSwapHorizontal,
numeric_state: mdiNumeric,
sun: mdiWeatherSunny,
conversation: mdiMicrophoneMessage,
tag: mdiNfcVariant,
template: mdiCodeBraces,
time: mdiClockOutline,
time_pattern: mdiAvTimer,
webhook: mdiWebhook,
persistent_notification: mdiMessageAlert,
zone: mdiMapMarkerRadius,
list: mdiFormatListBulleted,
};
export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
{
groups: {
device: {},
dynamicGroups: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
@@ -46,33 +83,3 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
"triggers" in trigger;
export interface TriggerDescription {
target?: TargetSelector["target"];
fields: Record<
string,
{
example?: string | boolean | number;
default?: unknown;
required?: boolean;
selector?: Selector;
context?: Record<string, string>;
}
>;
}
export type TriggerDescriptions = Record<string, TriggerDescription>;
export const subscribeTriggers = (
hass: HomeAssistant,
callback: (triggers: TriggerDescriptions) => void
) =>
hass.connection.subscribeMessage<TriggerDescriptions>(callback, {
type: "trigger_platforms/subscribe",
});
export const getTriggerDomain = (trigger: string) =>
trigger.includes(".") ? computeDomain(trigger) : trigger;
export const getTriggerObjectId = (trigger: string) =>
trigger.includes(".") ? computeObjectId(trigger) : "_";

View File

@@ -40,6 +40,12 @@
color: var(--primary-text-color, #212121);
height: 100vh;
}
@media (prefers-color-scheme: dark) {
html {
background-color: var(--primary-background-color, #111111);
color: var(--primary-text-color, #e1e1e1);
}
}
#ha-launch-screen {
position: fixed;
top: 0;
@@ -56,6 +62,12 @@
background-color: var(--primary-background-color, #fafafa);
z-index: 100;
}
@media (prefers-color-scheme: dark) {
/* body selector to avoid minification causing bad jinja2 */
body #ha-launch-screen {
background-color: var(--primary-background-color, #111111);
}
}
#ha-launch-screen.removing {
opacity: 0;
}
@@ -80,14 +92,6 @@
opacity: .66;
}
@media (prefers-color-scheme: dark) {
html {
background-color: var(--primary-background-color, #111111);
color: var(--primary-text-color, #e1e1e1);
}
/* body selector to avoid minification causing bad jinja2 */
body #ha-launch-screen {
background-color: var(--primary-background-color, #111111);
}
.ohf-logo {
filter: invert(1);
}

View File

@@ -14,13 +14,11 @@ import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import {
ACTION_BUILDING_BLOCKS,
getService,
isService,
VIRTUAL_ACTIONS,
} from "../../../../data/action";
import {
getValueFromDynamic,
isDynamic,
type AutomationClipboard,
} from "../../../../data/automation";
import type { AutomationClipboard } from "../../../../data/automation";
import type { Action } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import {
@@ -219,9 +217,9 @@ export default class HaAutomationAction extends LitElement {
actions = this.actions.concat(deepClone(this._clipboard!.action));
} else if (action in VIRTUAL_ACTIONS) {
actions = this.actions.concat(VIRTUAL_ACTIONS[action]);
} else if (isDynamic(action)) {
} else if (isService(action)) {
actions = this.actions.concat({
action: getValueFromDynamic(action),
action: getService(action),
metadata: {},
});
} else {

View File

@@ -1,3 +1,4 @@
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiClose,
@@ -5,7 +6,7 @@ import {
mdiPlus,
} from "@mdi/js";
import Fuse from "fuse.js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { SingleHassServiceTarget } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import {
@@ -21,12 +22,17 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeAreaName } from "../../../common/entity/compute_area_name";
import { computeDeviceName } from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display";
import { computeFloorName } from "../../../common/entity/compute_floor_name";
import { stringCompare } from "../../../common/string/compare";
import type {
LocalizeFunc,
LocalizeKeys,
} from "../../../common/translations/localize";
import { computeRTL } from "../../../common/util/compute_rtl";
import { debounce } from "../../../common/util/debounce";
import { deepEqual } from "../../../common/util/deep-equal";
import "../../../components/ha-bottom-sheet";
@@ -40,46 +46,50 @@ import "../../../components/ha-md-divider";
import "../../../components/ha-md-list";
import type { HaMdList } from "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-section-title";
import "../../../components/ha-service-icon";
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
import "../../../components/ha-wa-dialog";
import "../../../components/search-input";
import {
ACTION_BUILDING_BLOCKS_GROUP,
ACTION_COLLECTIONS,
ACTION_ICONS,
SERVICE_PREFIX,
getService,
isService,
} from "../../../data/action";
import {
DYNAMIC_PREFIX,
getValueFromDynamic,
isDynamic,
type AutomationElementGroup,
type AutomationElementGroupCollection,
getAreaDeviceLookup,
getAreaEntityLookup,
} from "../../../data/area_registry";
import type {
AutomationElementGroup,
AutomationElementGroupCollection,
} from "../../../data/automation";
import {
CONDITION_BUILDING_BLOCKS_GROUP,
CONDITION_COLLECTIONS,
CONDITION_ICONS,
} from "../../../data/condition";
import { getServiceIcons, getTriggerIcons } from "../../../data/icons";
import { fullEntitiesContext } from "../../../data/context";
import { getDeviceEntityLookup } from "../../../data/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import { getFloorAreaLookup } from "../../../data/floor_registry";
import { getServiceIcons } from "../../../data/icons";
import type { IntegrationManifest } from "../../../data/integration";
import {
domainToName,
fetchIntegrationManifests,
} from "../../../data/integration";
import type { TriggerDescriptions } from "../../../data/trigger";
import {
TRIGGER_COLLECTIONS,
getTriggerDomain,
getTriggerObjectId,
subscribeTriggers,
} from "../../../data/trigger";
import { TRIGGER_COLLECTIONS, TRIGGER_ICONS } from "../../../data/trigger";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { HaFuse } from "../../../resources/fuse";
import type { HomeAssistant } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import "./add-automation-element/ha-automation-add-from-target";
import type HaAutomationAddFromTarget from "./add-automation-element/ha-automation-add-from-target";
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
@@ -119,7 +129,7 @@ const ENTITY_DOMAINS_OTHER = new Set([
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
const ACTION_SERVICE_KEYWORDS = ["dynamicGroups", "helpers", "other"];
const ACTION_SERVICE_KEYWORDS = ["serviceGroups", "helpers", "other"];
@customElement("add-automation-element-dialog")
class DialogAddAutomationElement
@@ -134,7 +144,9 @@ class DialogAddAutomationElement
@state() private _selectedGroup?: string;
@state() private _tab: "groups" | "blocks" = "groups";
@state() private _selectedTarget?: SingleHassServiceTarget;
@state() private _tab: "targets" | "groups" | "blocks" = "targets";
@state() private _filter = "";
@@ -150,20 +162,26 @@ class DialogAddAutomationElement
@state() private _narrow = false;
@state() private _triggerDescriptions: TriggerDescriptions = {};
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
private fullEntities!: EntityRegistryEntry[];
@query(".items ha-md-list ha-md-list-item")
private _itemsListFirstElement?: HaMdList;
@query("ha-automation-add-from-target")
private _targetPickerElement?: HaAutomationAddFromTarget;
@query(".items")
private _itemsListElement?: HTMLDivElement;
@query(".content")
private _contentElement?: HTMLDivElement;
private _fullScreen = false;
private _removeKeyboardShortcuts?: () => void;
private _unsub?: Promise<UnsubscribeFunc>;
public showDialog(params): void {
this._params = params;
@@ -175,17 +193,6 @@ class DialogAddAutomationElement
this._calculateUsedDomains();
getServiceIcons(this.hass);
}
if (this._params?.type === "trigger") {
this.hass.loadBackendTranslation("triggers");
this._fetchManifests();
getTriggerIcons(this.hass);
this._unsub = subscribeTriggers(this.hass, (triggers) => {
this._triggerDescriptions = {
...this._triggerDescriptions,
...triggers,
};
});
}
this._fullScreen = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
@@ -199,10 +206,6 @@ class DialogAddAutomationElement
public closeDialog() {
this.removeKeyboardShortcuts();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
if (this._params) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -211,7 +214,8 @@ class DialogAddAutomationElement
this._bottomSheetMode = false;
this._params = undefined;
this._selectedGroup = undefined;
this._tab = "groups";
this._tab = "targets";
this._selectedTarget = undefined;
this._selectedCollectionIndex = undefined;
this._filter = "";
this._manifests = undefined;
@@ -344,11 +348,6 @@ class DialogAddAutomationElement
);
const items = flattenGroups(groups).flat();
if (type === "trigger") {
items.push(
...this._triggers(localize, this._triggerDescriptions, manifests)
);
}
if (type === "action") {
items.push(...this._services(localize, services, manifests));
}
@@ -371,7 +370,6 @@ class DialogAddAutomationElement
domains: Set<string> | undefined,
localize: LocalizeFunc,
services: HomeAssistant["services"],
triggerDescriptions: TriggerDescriptions,
manifests?: DomainManifestLookup
): {
titleKey?: LocalizeKeys;
@@ -395,32 +393,7 @@ class DialogAddAutomationElement
services,
manifests,
domains,
collection.groups.dynamicGroups
? undefined
: collection.groups.helpers
? "helper"
: "other"
)
);
collectionGroups = collectionGroups.filter(
([key]) => !ACTION_SERVICE_KEYWORDS.includes(key)
);
}
if (
type === "trigger" &&
Object.keys(collection.groups).some((item) =>
ACTION_SERVICE_KEYWORDS.includes(item)
)
) {
groups.push(
...this._triggerGroups(
localize,
triggerDescriptions,
manifests,
domains,
collection.groups.dynamicGroups
collection.groups.serviceGroups
? undefined
: collection.groups.helpers
? "helper"
@@ -487,19 +460,10 @@ class DialogAddAutomationElement
services: HomeAssistant["services"],
manifests?: DomainManifestLookup
): ListItem[] => {
if (type === "action" && isDynamic(group)) {
if (type === "action" && isService(group)) {
return this._services(localize, services, manifests, group);
}
if (type === "trigger" && isDynamic(group)) {
return this._triggers(
localize,
this._triggerDescriptions,
manifests,
group
);
}
const groups = this._getGroups(type, group, collectionIndex);
const result = Object.entries(groups).map(([key, options]) =>
@@ -581,7 +545,7 @@ class DialogAddAutomationElement
brand-fallback
></ha-domain-icon>
`,
key: `${DYNAMIC_PREFIX}${domain}`,
key: `${SERVICE_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
});
@@ -592,102 +556,6 @@ class DialogAddAutomationElement
);
};
private _triggerGroups = (
localize: LocalizeFunc,
triggers: TriggerDescriptions,
manifests: DomainManifestLookup | undefined,
domains: Set<string> | undefined,
type: "helper" | "other" | undefined
): ListItem[] => {
if (!triggers || !manifests) {
return [];
}
const result: ListItem[] = [];
const addedDomains = new Set<string>();
Object.keys(triggers).forEach((trigger) => {
const domain = getTriggerDomain(trigger);
if (addedDomains.has(domain)) {
return;
}
addedDomains.add(domain);
const manifest = manifests[domain];
const domainUsed = !domains ? true : domains.has(domain);
if (
(type === undefined &&
(ENTITY_DOMAINS_MAIN.has(domain) ||
(manifest?.integration_type === "entity" &&
domainUsed &&
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
(type === "helper" && manifest?.integration_type === "helper") ||
(type === "other" &&
!ENTITY_DOMAINS_MAIN.has(domain) &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
(!domainUsed && manifest?.integration_type === "entity") ||
!["helper", "entity"].includes(manifest?.integration_type || "")))
) {
result.push({
icon: html`
<ha-domain-icon
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>
`,
key: `${DYNAMIC_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
});
}
});
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
};
private _triggers = memoizeOne(
(
localize: LocalizeFunc,
triggers: TriggerDescriptions,
_manifests: DomainManifestLookup | undefined,
group?: string
): ListItem[] => {
if (!triggers) {
return [];
}
const result: ListItem[] = [];
for (const trigger of Object.keys(triggers)) {
const domain = getTriggerDomain(trigger);
const triggerName = getTriggerObjectId(trigger);
if (group && group !== `${DYNAMIC_PREFIX}${domain}`) {
continue;
}
result.push({
icon: html`
<ha-trigger-icon
.hass=${this.hass}
.trigger=${trigger}
></ha-trigger-icon>
`,
key: `${DYNAMIC_PREFIX}${trigger}`,
name:
localize(`component.${domain}.triggers.${triggerName}.name`) ||
trigger,
description:
localize(
`component.${domain}.triggers.${triggerName}.description`
) || trigger,
});
}
return result;
}
);
private _services = memoizeOne(
(
localize: LocalizeFunc,
@@ -702,8 +570,8 @@ class DialogAddAutomationElement
let domain: string | undefined;
if (isDynamic(group)) {
domain = getValueFromDynamic(group!);
if (isService(group)) {
domain = getService(group!);
}
const addDomain = (dmn: string) => {
@@ -717,7 +585,7 @@ class DialogAddAutomationElement
.service=${`${dmn}.${service}`}
></ha-service-icon>
`,
key: `${DYNAMIC_PREFIX}${dmn}.${service}`,
key: `${SERVICE_PREFIX}${dmn}.${service}`,
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
services[dmn][service]?.name ||
@@ -794,10 +662,11 @@ class DialogAddAutomationElement
private _renderContent() {
const automationElementType = this._params!.type;
const items = this._filter
const items =
this._filter || this._selectedTarget
? this._getFilteredItems(
automationElementType,
this._filter,
this._filter || "Ti",
this.hass.localize,
this.hass.services,
this._manifests
@@ -831,39 +700,28 @@ class DialogAddAutomationElement
this._domains,
this.hass.localize,
this.hass.services,
this._triggerDescriptions,
this._manifests
);
const groupName = isDynamic(this._selectedGroup)
? domainToName(
this.hass.localize,
getValueFromDynamic(this._selectedGroup!),
this._manifests?.[getValueFromDynamic(this._selectedGroup!)]
)
: this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys
) ||
this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.type.${this._selectedGroup}.label` as LocalizeKeys
);
const typeTitle = this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.add`
);
const tabButtons = [
{
label: this.hass.localize(`ui.panel.config.automation.editor.targets`),
value: "targets",
},
{
label: this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.name`
),
value: "groups",
},
{
];
if (this._params?.type !== "trigger") {
tabButtons.push({
label: this.hass.localize(`ui.panel.config.automation.editor.blocks`),
value: "blocks",
},
];
});
}
const hideCollections =
this._filter ||
@@ -872,28 +730,8 @@ class DialogAddAutomationElement
return html`
<div slot="header">
<ha-dialog-header subtitle-position="above">
<span slot="title"
>${this._narrow && this._selectedGroup
? groupName
: typeTitle}</span
>
${this._narrow && this._selectedGroup
? html`<span slot="subtitle">${typeTitle}</span>`
: nothing}
${this._narrow && this._selectedGroup
? html`<ha-icon-button-prev
slot="navigationIcon"
@click=${this._back}
></ha-icon-button-prev>`
: html`<ha-icon-button
.path=${mdiClose}
@click=${this._close}
slot="navigationIcon"
></ha-icon-button>`}
</ha-dialog-header>
${!this._narrow || !this._selectedGroup
${this._renderHeader()}
${!this._narrow || (!this._selectedGroup && !this._selectedTarget)
? html`
<search-input
?autofocus=${!this._narrow}
@@ -906,9 +744,8 @@ class DialogAddAutomationElement
></search-input>
`
: nothing}
${this._params?.type !== "trigger" &&
!this._filter &&
(!this._narrow || !this._selectedGroup)
${!this._filter &&
(!this._narrow || (!this._selectedGroup && !this._selectedTarget))
? html`<ha-button-toggle-group
variant="neutral"
active-variant="brand"
@@ -920,7 +757,21 @@ class DialogAddAutomationElement
></ha-button-toggle-group>`
: nothing}
</div>
<div class="content">
<div
class=${classMap({
content: true,
column: this._narrow && this._selectedTarget,
})}
>
${this._tab === "targets"
? html`<ha-automation-add-from-target
.hass=${this.hass}
.value=${this._selectedTarget}
@value-changed=${this._handleTargetSelected}
.narrow=${this._narrow}
class=${this._getAddFromTargetHidden()}
></ha-automation-add-from-target>`
: html`
<ha-md-list
class=${classMap({
groups: true,
@@ -976,14 +827,17 @@ class DialogAddAutomationElement
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
<ha-md-divider
role="separator"
tabindex="-1"
></ha-md-divider>`
: nothing}
${collections.map(
(collection, index) => html`
${collection.titleKey
? html`<div class="collection-title">
? html`<ha-section-title>
${this.hass.localize(collection.titleKey)}
</div>`
</ha-section-title>`
: nothing}
${repeat(
collection.groups,
@@ -995,7 +849,9 @@ class DialogAddAutomationElement
.value=${item.key}
.index=${index}
@click=${this._groupSelected}
class=${item.key === this._selectedGroup ? "selected" : ""}
class=${item.key === this._selectedGroup
? "selected"
: ""}
>
<div slot="headline">${item.name}</div>
${item.icon
@@ -1012,18 +868,27 @@ class DialogAddAutomationElement
`
)}
</ha-md-list>
`}
<div
class=${classMap({
items: true,
blank:
!this._selectedGroup && !this._filter && this._tab === "groups",
(this._tab === "groups" &&
!this._selectedGroup &&
!this._filter) ||
(this._tab === "targets" &&
!this._selectedTarget &&
!this._filter),
"empty-search":
!items?.length && !filteredBlockItems?.length && this._filter,
hidden:
this._narrow &&
!this._selectedGroup &&
(!this._selectedTarget ||
(this._selectedTarget &&
!Object.values(this._selectedTarget)[0])) &&
!this._filter &&
this._tab === "groups",
this._tab !== "blocks",
})}
@scroll=${this._onItemsScroll}
>
@@ -1037,6 +902,10 @@ class DialogAddAutomationElement
? this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.select`
)
: this._tab === "targets" && !this._selectedTarget && !this._filter
? this.hass.localize(
`ui.panel.config.automation.editor.select_target`
)
: !items?.length &&
this._filter &&
(!filteredBlockItems || !filteredBlockItems.length)
@@ -1149,6 +1018,10 @@ class DialogAddAutomationElement
}
private _back() {
if (this._selectedTarget) {
this._targetPickerElement?.navigateBack();
return;
}
this._selectedGroup = undefined;
}
@@ -1172,6 +1045,21 @@ class DialogAddAutomationElement
this.closeDialog();
}
private _handleTargetSelected = (
ev: CustomEvent<{ value: SingleHassServiceTarget }>
) => {
this._selectedTarget = ev.detail.value;
// TODO fix on mobile
requestAnimationFrame(() => {
if (this._narrow) {
this._contentElement?.scrollTo(0, 0);
} else {
this._itemsListElement?.scrollTo(0, 0);
}
});
};
private _debounceFilterChanged = debounce(
(ev) => this._filterChanged(ev),
200
@@ -1268,6 +1156,183 @@ class DialogAddAutomationElement
this.closeDialog();
};
private _renderHeader() {
return html`
<ha-dialog-header subtitle-position="above">
<span slot="title">${this._getDialogTitle()}</span>
${this._renderDialogSubtitle()}
${this._narrow && (this._selectedGroup || this._selectedTarget)
? html`<ha-icon-button-prev
slot="navigationIcon"
@click=${this._back}
></ha-icon-button-prev>`
: html`<ha-icon-button
.path=${mdiClose}
@click=${this._close}
slot="navigationIcon"
></ha-icon-button>`}
</ha-dialog-header>
`;
}
private _getDialogTitle() {
if (this._narrow && this._selectedGroup) {
return isService(this._selectedGroup)
? domainToName(
this.hass.localize,
getService(this._selectedGroup!),
this._manifests?.[getService(this._selectedGroup!)]
)
: this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys
) ||
this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.type.${this._selectedGroup}.label` as LocalizeKeys
);
}
if (this._narrow && this._selectedTarget) {
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
this._selectedTarget
);
if (targetId === undefined && targetType === "floor") {
return this.hass.localize("ui.components.area-picker.unassigned_areas");
}
if (targetId) {
if (targetType === "floor") {
return computeFloorName(this.hass.floors[targetId]) || targetId;
}
if (targetType === "area") {
return computeAreaName(this.hass.areas[targetId]) || targetId;
}
if (targetType === "device") {
return computeDeviceName(this.hass.devices[targetId]) || targetId;
}
if (targetType === "entity" && this.hass.states[targetId]) {
const stateObj = this.hass.states[targetId];
const [entityName, deviceName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
return entityName || deviceName || targetId;
}
}
}
return this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.add`
);
}
private _renderDialogSubtitle() {
if (!this._narrow) {
return nothing;
}
if (this._selectedGroup) {
return html`<span slot="subtitle"
>${this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.add`
)}</span
>`;
}
if (this._selectedTarget) {
let subtitle: string | undefined;
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
this._selectedTarget
);
if (targetId) {
if (targetType === "area" && this.hass.areas[targetId].floor_id) {
const floorId = this.hass.areas[targetId].floor_id;
subtitle = computeFloorName(this.hass.floors[floorId]) || floorId;
}
if (targetType === "device" && this.hass.devices[targetId].area_id) {
const areaId = this.hass.devices[targetId].area_id;
subtitle = computeAreaName(this.hass.areas[areaId]) || areaId;
}
if (targetType === "entity" && this.hass.states[targetId]) {
const stateObj = this.hass.states[targetId];
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
subtitle = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
}
}
if (subtitle) {
return html`<span slot="subtitle">${subtitle}</span>`;
}
}
return nothing;
}
private _getFloorAreaLookupMemoized = memoizeOne(
(areas: HomeAssistant["areas"]) => getFloorAreaLookup(Object.values(areas))
);
private _getAreaDeviceLookupMemoized = memoizeOne(
(devices: HomeAssistant["devices"]) =>
getAreaDeviceLookup(Object.values(devices))
);
private _getAreaEntityLookupMemoized = memoizeOne(getAreaEntityLookup);
private _getDeviceEntityLookupMemoized = memoizeOne(getDeviceEntityLookup);
private _extractTypeAndIdFromTarget = memoizeOne(
(target: SingleHassServiceTarget): [string, string | undefined] => {
const [targetTypeId, targetId] = Object.entries(target)[0];
const targetType = targetTypeId.replace("_id", "");
return [targetType, targetId];
}
);
private _getAddFromTargetHidden() {
if (this._narrow && this._selectedTarget) {
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
this._selectedTarget
);
if (
targetId &&
((targetType === "floor" &&
this._getFloorAreaLookupMemoized(this.hass.areas)[targetId]
?.length === 0) ||
(targetType === "area" &&
this._getAreaDeviceLookupMemoized(this.hass.devices)[targetId]
?.length === 0 &&
this._getAreaEntityLookupMemoized(this.fullEntities, true)[targetId]
?.length === 0) ||
(targetType === "device" &&
this._getDeviceEntityLookupMemoized(this.fullEntities, true)[
targetId
]?.length === 0))
) {
return "hidden";
}
}
return "";
}
static get styles(): CSSResultGroup {
return [
css`
@@ -1321,10 +1386,15 @@ class DialogAddAutomationElement
display: flex;
}
.content.column {
flex-direction: column;
}
ha-md-list {
padding: 0;
}
ha-automation-add-from-target,
.groups {
overflow: auto;
flex: 3;
@@ -1332,6 +1402,13 @@ class DialogAddAutomationElement
border: 1px solid var(--ha-color-border-neutral-quiet);
margin: var(--ha-space-3);
margin-inline-end: var(--ha-space-0);
}
ha-automation-add-from-target.hidden {
display: none;
}
.groups {
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-1);
@@ -1339,7 +1416,8 @@ class DialogAddAutomationElement
--md-list-item-supporting-text-font: var(--ha-font-size-s);
--md-list-item-one-line-container-height: var(--ha-space-10);
}
ha-bottom-sheet .groups {
ha-bottom-sheet .groups,
ha-bottom-sheet ha-automation-add-from-target {
margin: var(--ha-space-3);
}
.groups .selected {
@@ -1351,16 +1429,9 @@ class DialogAddAutomationElement
color: var(--ha-color-on-primary-normal);
}
.collection-title {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
ha-section-title {
top: 0;
position: sticky;
min-height: var(--ha-space-6);
display: flex;
align-items: center;
z-index: 1;
}
@@ -1371,6 +1442,11 @@ class DialogAddAutomationElement
flex: 7;
}
.content.column ha-automation-add-from-target,
.content.column .items {
flex: none;
}
ha-wa-dialog .items {
margin-top: var(--ha-space-3);
}

View File

@@ -0,0 +1,810 @@
import "@home-assistant/webawesome/dist/components/tree-item/tree-item";
import "@home-assistant/webawesome/dist/components/tree/tree";
import type { WaSelectionChangeEvent } from "@home-assistant/webawesome/dist/events/selection-change";
import { consume } from "@lit/context";
import { mdiSelectionMarker, mdiTextureBox } from "@mdi/js";
import type { SingleHassServiceTarget } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-floor-icon";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-section-title";
import "../../../../components/ha-svg-icon";
import {
getAreasNestedInFloors,
type AreaFloorValue,
type FloorComboBoxItem,
type FloorNestedComboBoxItem,
type UnassignedAreasFloorComboBoxItem,
} from "../../../../data/area_floor";
import {
getConfigEntries,
type ConfigEntry,
} from "../../../../data/config_entries";
import {
areasContext,
devicesContext,
entitiesContext,
floorsContext,
labelsContext,
localizeContext,
statesContext,
} from "../../../../data/context";
import {
getLabels,
type LabelRegistryEntry,
} from "../../../../data/label_registry";
import { extractFromTarget } from "../../../../data/target";
import type { HomeAssistant } from "../../../../types";
import { brandsUrl } from "../../../../util/brands-url";
const SEPARATOR = "________";
interface DeviceEntries {
open: boolean;
entities: string[];
}
interface AreaEntries {
open: boolean;
devices: Record<string, DeviceEntries>;
entities: string[];
}
@customElement("ha-automation-add-from-target")
export default class HaAutomationAddFromTarget extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public value?: SingleHassServiceTarget;
@property({ type: Boolean }) public narrow = false;
// #region context
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: HomeAssistant["localize"];
@state()
@consume({ context: statesContext, subscribe: true })
private states!: HomeAssistant["states"];
@state()
@consume({ context: floorsContext, subscribe: true })
private floors!: HomeAssistant["floors"];
@state()
@consume({ context: areasContext, subscribe: true })
private areas!: HomeAssistant["areas"];
@state()
@consume({ context: devicesContext, subscribe: true })
private devices!: HomeAssistant["devices"];
@state()
@consume({ context: entitiesContext, subscribe: true })
private entities!: HomeAssistant["entities"];
@state()
@consume({ context: labelsContext, subscribe: true })
private _labelRegistry!: LabelRegistryEntry[];
// #endregion context
@state()
private _floorAreas: (
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] = [];
@state()
private _areaEntries: Record<string, AreaEntries> = {};
private _getLabelsMemoized = memoizeOne(getLabels);
private _configEntryLookup: Record<string, ConfigEntry> = {};
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._loadConfigEntries();
this._getTreeData();
}
}
protected render() {
return html`
${!this.narrow || !this.value ? this._renderFloors() : nothing}
${this.narrow && this.value ? this._renderNarrow(this.value) : nothing}
${!this.narrow || !this.value ? this._renderLabels() : nothing}
`;
}
private _renderNarrow(value: SingleHassServiceTarget) {
const [valueTypeId, valueId] = Object.entries(value)[0];
const valueType = valueTypeId.replace("_id", "");
if (!valueType || valueType === "label") {
return nothing;
}
if (valueType === "floor") {
return this._renderAreas(
this._floorAreas.find(
(floor) =>
(valueId && floor.id === `${valueType}${SEPARATOR}${valueId}`) ||
(!valueId && !floor.id)
)?.areas
);
}
if (valueType === "area") {
const { devices, entities } =
this._areaEntries[`area${SEPARATOR}${valueId}`];
const numberOfDevices = Object.keys(devices).length;
return html`
${numberOfDevices ? this._renderDevices(devices) : nothing}
${entities.length ? this._renderEntities(entities) : nothing}
`;
}
if (valueType === "device" && this.devices[valueId]) {
const deviceArea = this.devices[valueId].area_id!;
return this._renderEntities(
this._areaEntries[`area${SEPARATOR}${deviceArea}`]?.devices[valueId]
?.entities
);
}
return nothing;
}
private _renderFloors() {
return html`<ha-section-title
>${this.localize(
"ui.panel.config.automation.editor.home"
)}</ha-section-title
>
${!this._floorAreas.length ||
(!this._floorAreas[0].id && !this._floorAreas[0].areas.length)
? html`<ha-md-list>
<ha-md-list-item type="text">
<div slot="headline">
${this.localize("ui.components.area-picker.no_areas")}
</div>
</ha-md-list-item>
</ha-md-list>`
: this.narrow
? html`<ha-md-list>
${this._floorAreas.map((floor, index) =>
index === 0 && !floor.id
? this._renderAreas(floor.areas)
: html`<ha-md-list-item
interactive
type="button"
.target=${floor.id || `floor${SEPARATOR}`}
@click=${this._selectItem}
>
${floor.id && (floor as FloorNestedComboBoxItem).floor
? html`<ha-floor-icon
slot="start"
.floor=${(floor as FloorNestedComboBoxItem).floor}
></ha-floor-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiSelectionMarker}
></ha-svg-icon>`}
<div slot="headline">
${!floor.id
? this.localize(
"ui.components.area-picker.unassigned_areas"
)
: floor.primary}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`
)}
</ha-md-list>`
: html`<wa-tree @wa-selection-change=${this._handleSelectionChange}>
${this._floorAreas.map((floor, index) =>
index === 0 && !floor.id
? this._renderAreas(floor.areas)
: html`<wa-tree-item
.disabledSelection=${!floor.id}
.target=${floor.id}
.selected=${!!floor.id &&
this._getSelectedTargetId(this.value) === floor.id}
>
${floor.id && (floor as FloorNestedComboBoxItem).floor
? html`<ha-floor-icon
.floor=${(floor as FloorNestedComboBoxItem).floor}
></ha-floor-icon>`
: html`<ha-svg-icon
.path=${mdiSelectionMarker}
></ha-svg-icon>`}
${!floor.id
? this.localize(
"ui.components.area-picker.unassigned_areas"
)
: floor.primary}
${this._renderAreas(floor.areas)}
</wa-tree-item>`
)}
</wa-tree>`} `;
}
private _renderLabels() {
const labels = this._getLabelsMemoized(
this.states,
this.areas,
this.devices,
this.entities,
this._labelRegistry,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
`label${SEPARATOR}`
);
if (!labels.length) {
return nothing;
}
return html`<ha-section-title
>${this.localize("ui.components.label-picker.labels")}</ha-section-title
>
<ha-md-list>
${labels.map(
(label) =>
html`<ha-md-list-item
interactive
type="button"
.target=${label.id}
@click=${this._selectItem}
class=${this._getSelectedTargetId(this.value) === label.id
? "selected"
: ""}
>${label.icon
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
: label.icon_path
? html`<ha-svg-icon
slot="start"
.path=${label.icon_path}
></ha-svg-icon>`
: nothing}
<div slot="headline">${label.primary}</div>
${this.narrow
? html`<ha-icon-next slot="end"></ha-icon-next> `
: nothing}
</ha-md-list-item>`
)}
</ha-md-list>`;
}
private _renderAreas(areas: FloorComboBoxItem[] = []) {
if (!areas.length) {
return nothing;
}
if (this.narrow) {
return html`<ha-section-title
>${this.localize(
"ui.components.target-picker.type.areas"
)}</ha-section-title
>
<ha-md-list>
${areas.map(({ id, primary, icon, icon_path }) => {
if (!this._areaEntries[id]) {
return nothing;
}
return html`<ha-md-list-item
interactive
type="button"
.target=${id}
@click=${this._selectItem}
>
${icon
? html`<ha-icon slot="start" .icon=${icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${icon_path || mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${primary}</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`;
})}
</ha-md-list>`;
}
return areas.map(({ id, primary, icon, icon_path }) => {
if (!this._areaEntries[id]) {
return nothing;
}
const { open, devices, entities } = this._areaEntries[id];
const numberOfDevices = Object.keys(devices).length;
const numberOfItems = numberOfDevices + entities.length;
return html`<wa-tree-item
.target=${id}
.selected=${this._getSelectedTargetId(this.value) === id}
.lazy=${!open && !!numberOfItems}
@wa-lazy-load=${this._expandItem}
@wa-collapse=${this._collapseItem}
>
${icon
? html`<ha-icon .icon=${icon}></ha-icon>`
: html`<ha-svg-icon
.path=${icon_path || mdiTextureBox}
></ha-svg-icon>`}
${primary}
${open
? html`
${numberOfDevices ? this._renderDevices(devices) : nothing}
${entities.length ? this._renderEntities(entities) : nothing}
`
: nothing}
</wa-tree-item>`;
});
}
private _renderDevices(devices: Record<string, DeviceEntries>) {
const renderedDevices = Object.keys(devices).map((deviceId) => {
if (!this.devices[deviceId]) {
return nothing;
}
const device = this.devices[deviceId];
const configEntry = device.primary_config_entry
? this._configEntryLookup?.[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
const deviceName = computeDeviceName(device) || deviceId;
if (this.narrow) {
return html`<ha-md-list-item
interactive
type="button"
.target=${`device${SEPARATOR}${deviceId}`}
@click=${this._selectItem}
>
${domain
? html`
<img
slot="start"
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
/>
`
: nothing}
<div slot="headline">${deviceName}</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`;
}
const { open, entities } = devices[deviceId];
return html`<wa-tree-item
.target=${`device${SEPARATOR}${deviceId}`}
.selected=${this._getSelectedTargetId(this.value) ===
`device${SEPARATOR}${deviceId}`}
.lazy=${!open && !!entities.length}
@wa-lazy-load=${this._expandItem}
@wa-collapse=${this._collapseItem}
.title=${deviceName}
>
${domain
? html`
<img
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
/>
`
: nothing}
<span class="item-label">${deviceName}</span>
${open ? this._renderEntities(entities) : nothing}
</wa-tree-item>`;
});
if (this.narrow) {
return html`<ha-section-title
>${this.localize(
"ui.components.target-picker.type.devices"
)}</ha-section-title
>
<ha-md-list> ${renderedDevices} </ha-md-list>`;
}
return renderedDevices;
}
private _renderEntities(entities: string[] = []) {
if (!entities.length) {
return nothing;
}
const renderedEntites = entities.map((entityId) => {
const stateObj = this.hass.states[entityId];
const [entityName, deviceName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.entities,
this.devices,
this.areas,
this.floors
);
const label = entityName || deviceName || entityId;
if (this.narrow) {
return html`<ha-md-list-item
interactive
type="button"
.target=${`entity${SEPARATOR}${entityId}`}
@click=${this._selectItem}
>
<state-badge
slot="start"
.stateObj=${stateObj}
.hass=${this.hass}
></state-badge>
<div slot="headline" class="item-label">${label}</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`;
}
return html`<wa-tree-item
.target=${`entity${SEPARATOR}${entityId}`}
.selected=${this._getSelectedTargetId(this.value) ===
`entity${SEPARATOR}${entityId}`}
.title=${label}
>
<state-badge .stateObj=${stateObj} .hass=${this.hass}></state-badge>
<span class="item-label">${label}</span>
</wa-tree-item>`;
});
if (this.narrow) {
return html`<ha-section-title
>${this.localize(
"ui.components.target-picker.type.entities"
)}</ha-section-title
>
<ha-md-list>${renderedEntites}</ha-md-list>`;
}
return renderedEntites;
}
private _getSelectedTargetId = memoizeOne(
(value: SingleHassServiceTarget | undefined) =>
value && Object.keys(value).length
? `${Object.keys(value)[0].replace("_id", "")}${SEPARATOR}${Object.values(value)[0]}`
: undefined
);
private _getTreeData() {
this._floorAreas = getAreasNestedInFloors(
this.states,
this.floors,
this.areas,
this.devices,
this.entities,
this._formatId,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
true
);
for (const floor of this._floorAreas) {
for (const area of floor.areas) {
this._loadArea(area);
}
}
}
private _formatId = memoizeOne((value: AreaFloorValue): string =>
[value.type, value.id].join(SEPARATOR)
);
private _handleSelectionChange(ev: WaSelectionChangeEvent) {
const treeItem = ev.detail.selection[0] as unknown as
| { target?: string }
| undefined;
if (treeItem?.target) {
this._valueChanged(treeItem.target);
}
}
private _selectItem(ev: CustomEvent) {
const target = (ev.currentTarget as any).target;
if (target) {
this._valueChanged(target);
}
}
private _valueChanged(itemId: string) {
const [type, id] = itemId.split(SEPARATOR, 2);
fireEvent(this, "value-changed", {
value: { [`${type}_id`]: id || undefined },
});
}
private async _loadArea(area: FloorComboBoxItem) {
try {
const [, id] = area.id.split(SEPARATOR, 2);
const targetEntries = await extractFromTarget(this.hass, {
area_id: id,
});
const devices: Record<string, DeviceEntries> = {};
targetEntries.referenced_devices.forEach((device_id) => {
devices[device_id] = {
open: false,
entities: [],
};
});
const entities: string[] = [];
targetEntries.referenced_entities.forEach((entity_id) => {
const entity = this.hass.entities[entity_id];
if (entity.device_id && devices[entity.device_id]) {
devices[entity.device_id].entities.push(entity_id);
} else {
entities.push(entity_id);
}
});
this._areaEntries = {
...this._areaEntries,
[area.id]: {
open: false,
devices,
entities,
},
};
} catch (e) {
// eslint-disable-next-line no-console
console.error("Failed to extract target", e);
}
}
private _expandItem(ev) {
const targetId = ev.target.target;
const [type, id] = targetId.split(SEPARATOR, 2);
if (type === "area") {
this._areaEntries = {
...this._areaEntries,
[targetId]: {
...this._areaEntries[targetId],
open: true,
},
};
} else if (type === "device") {
const areaEntry = Object.values(this._areaEntries).find((area) =>
Object.keys(area.devices).includes(id)
);
if (areaEntry) {
areaEntry.devices[id].open = true;
this._areaEntries = {
...this._areaEntries,
};
}
}
}
private _collapseItem(ev) {
const targetId = ev.target.target;
const [type, id] = targetId.split(SEPARATOR, 2);
if (type === "area") {
this._areaEntries = {
...this._areaEntries,
[targetId]: {
...this._areaEntries[targetId],
open: false,
},
};
} else if (type === "device") {
const areaEntry = Object.values(this._areaEntries).find((area) =>
Object.keys(area.devices).includes(id)
);
if (areaEntry) {
areaEntry.devices[id].open = false;
this._areaEntries = {
...this._areaEntries,
};
}
}
}
private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries(
configEntries.map((entry) => [entry.entry_id, entry])
);
}
public navigateBack() {
if (!this.value) {
return;
}
const valueType = Object.keys(this.value)[0].replace("_id", "");
const valueId = this.value[`${valueType}_id`];
if (valueType === "floor" || valueType === "label") {
fireEvent(this, "value-changed", { value: undefined });
}
if (valueType === "area") {
fireEvent(this, "value-changed", {
value: { floor_id: this.areas[valueId].floor_id },
});
return;
}
if (valueType === "device") {
fireEvent(this, "value-changed", {
value: { area_id: this.devices[valueId].area_id },
});
}
if (valueType === "entity") {
for (const [areaId, areaEntry] of Object.entries(this._areaEntries)) {
const entityDeviceId = this.entities[valueId].device_id;
if (entityDeviceId && areaEntry.devices[entityDeviceId]) {
// Device is also in area -> go back to device
break;
}
if (areaEntry.entities.includes(valueId)) {
fireEvent(this, "value-changed", {
value: { area_id: areaId.split(SEPARATOR, 2)[1] },
});
return;
}
}
fireEvent(this, "value-changed", {
value: { device_id: this.entities[valueId].device_id },
});
}
}
static styles = css`
:host {
--wa-color-neutral-fill-quiet: var(--ha-color-fill-primary-normal-active);
}
ha-section-title {
top: 0;
position: sticky;
z-index: 1;
}
wa-tree-item::part(item) {
height: var(--ha-space-10);
padding: var(--ha-space-1) var(--ha-space-3);
cursor: pointer;
border-inline-start: 0;
}
wa-tree-item::part(label) {
gap: var(--ha-space-3);
font-family: var(--ha-font-family-heading);
font-weight: var(--ha-font-weight-medium);
overflow: hidden;
}
.item-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
ha-svg-icon,
ha-icon,
ha-floor-icon {
padding: var(--ha-space-1);
color: var(--ha-color-on-neutral-quiet);
}
wa-tree-item::part(item):hover {
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
img {
width: 24px;
height: 24px;
padding: var(--ha-space-1);
}
state-badge {
min-width: 32px;
max-width: 32px;
min-height: 32px;
max-height: 32px;
}
wa-tree-item[selected],
wa-tree-item[selected] > ha-svg-icon,
wa-tree-item[selected] > ha-icon,
wa-tree-item[selected] > ha-floor-icon {
color: var(--ha-color-on-primary-normal);
}
wa-tree-item[selected]::part(item):hover {
background-color: var(--ha-color-fill-primary-normal-hover);
}
wa-tree-item::part(base).tree-item-selected .item {
background-color: yellow;
}
ha-md-list {
padding: 0;
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-1);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-size-s);
--md-list-item-one-line-container-height: var(--ha-space-10);
}
ha-md-list-item.selected {
background-color: var(--ha-color-fill-primary-normal-active);
--md-list-item-label-text-color: var(--ha-color-on-primary-normal);
--icon-primary-color: var(--ha-color-on-primary-normal);
}
ha-md-list-item.selected ha-icon,
ha-md-list-item.selected ha-svg-icon {
color: var(--ha-color-on-primary-normal);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-add-from-target": HaAutomationAddFromTarget;
}
}

View File

@@ -28,6 +28,7 @@ import type HaAutomationConditionEditor from "../action/ha-automation-action-edi
import { getAutomationActionType } from "../action/ha-automation-action-row";
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "../trigger/ha-automation-trigger-editor";
import "./ha-automation-sidebar-card";
@customElement("ha-automation-sidebar-action")

View File

@@ -17,6 +17,7 @@ import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";

View File

@@ -6,9 +6,6 @@ import {
} from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-svg-icon";
import type { OptionSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";

View File

@@ -15,15 +15,8 @@ import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import type {
LegacyTrigger,
TriggerSidebarConfig,
} from "../../../../data/automation";
import {
getTriggerDomain,
getTriggerObjectId,
isTriggerList,
} from "../../../../data/trigger";
import type { TriggerSidebarConfig } from "../../../../data/automation";
import { isTriggerList } from "../../../../data/trigger";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { overflowStyles, sidebarEditorStyles } from "../styles";
@@ -70,7 +63,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
protected render() {
const rowDisabled =
"enabled" in this.config.config && this.config.config.enabled === false;
this.disabled ||
("enabled" in this.config.config && this.config.config.enabled === false);
const type = isTriggerList(this.config.config)
? "list"
: this.config.config.trigger;
@@ -79,18 +73,9 @@ export default class HaAutomationSidebarTrigger extends LitElement {
"ui.panel.config.automation.editor.triggers.trigger"
);
const domain =
"trigger" in this.config.config &&
getTriggerDomain(this.config.config.trigger);
const triggerName =
"trigger" in this.config.config &&
getTriggerObjectId(this.config.config.trigger);
const title =
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.${type as LegacyTrigger["trigger"]}.label`
) ||
this.hass.localize(`component.${domain}.triggers.${triggerName}.name`);
const title = this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.${type}.label`
);
return html`
<ha-automation-sidebar-card
@@ -284,7 +269,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
class="sidebar-editor"
.hass=${this.hass}
.trigger=${this.config.config}
.description=${this.config.description}
@value-changed=${this._valueChangedSidebar}
@yaml-changed=${this._yamlChangedSidebar}
.uiSupported=${this.config.uiSupported}

View File

@@ -9,12 +9,10 @@ import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { Trigger } from "../../../../data/automation";
import { migrateAutomationTrigger } from "../../../../data/automation";
import type { TriggerDescription } from "../../../../data/trigger";
import { isTriggerList } from "../../../../data/trigger";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
import "./types/ha-automation-trigger-platform";
@customElement("ha-automation-trigger-editor")
export default class HaAutomationTriggerEditor extends LitElement {
@@ -33,8 +31,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
@property({ attribute: false }) public description?: TriggerDescription;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
protected render() {
@@ -91,14 +87,7 @@ export default class HaAutomationTriggerEditor extends LitElement {
`
: nothing}
<div @value-changed=${this._onUiChanged}>
${this.description
? html`<ha-automation-trigger-platform
.hass=${this.hass}
.trigger=${this.trigger}
.description=${this.description}
.disabled=${this.disabled}
></ha-automation-trigger-platform>`
: dynamicElement(`ha-automation-trigger-${type}`, {
${dynamicElement(`ha-automation-trigger-${type}`, {
hass: this.hass,
trigger: this.trigger,
disabled: this.disabled,

View File

@@ -40,11 +40,9 @@ import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-svg-icon";
import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon";
import type {
AutomationClipboard,
Trigger,
TriggerList,
TriggerSidebarConfig,
} from "../../../../data/automation";
import { isTrigger, subscribeTrigger } from "../../../../data/automation";
@@ -52,8 +50,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import type { TriggerDescriptions } from "../../../../data/trigger";
import { isTriggerList } from "../../../../data/trigger";
import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger";
import {
showAlertDialog,
showPromptDialog,
@@ -75,7 +72,6 @@ import "./types/ha-automation-trigger-list";
import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-persistent_notification";
import "./types/ha-automation-trigger-platform";
import "./types/ha-automation-trigger-state";
import "./types/ha-automation-trigger-sun";
import "./types/ha-automation-trigger-tag";
@@ -141,9 +137,6 @@ export default class HaAutomationTriggerRow extends LitElement {
@state() private _warnings?: string[];
@property({ attribute: false })
public triggerDescriptions: TriggerDescriptions = {};
@property({ type: Boolean }) public narrow = false;
@query("ha-automation-trigger-editor")
@@ -185,24 +178,18 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _renderRow() {
const type = this._getType(this.trigger, this.triggerDescriptions);
const type = this._getType(this.trigger);
const supported = this._uiSupported(type);
const yamlMode = this._yamlMode || !supported;
return html`
${type === "list"
? html`<ha-svg-icon
<ha-svg-icon
slot="leading-icon"
class="trigger-icon"
.path=${TRIGGER_ICONS[type]}
></ha-svg-icon>`
: html`<ha-trigger-icon
slot="leading-icon"
.hass=${this.hass}
.trigger=${(this.trigger as Exclude<Trigger, TriggerList>).trigger}
></ha-trigger-icon>`}
></ha-svg-icon>
<h3 slot="header">
${describeTrigger(this.trigger, this.hass, this._entityReg)}
</h3>
@@ -406,9 +393,6 @@ export default class HaAutomationTriggerRow extends LitElement {
<ha-automation-trigger-editor
.hass=${this.hass}
.trigger=${this.trigger}
.description=${"trigger" in this.trigger
? this.triggerDescriptions[this.trigger.trigger]
: undefined}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.uiSupported=${supported}
@@ -568,7 +552,6 @@ export default class HaAutomationTriggerRow extends LitElement {
}
public openSidebar(trigger?: Trigger): void {
trigger = trigger || this.trigger;
fireEvent(this, "open-sidebar", {
save: (value) => {
fireEvent(this, "value-changed", { value });
@@ -593,14 +576,8 @@ export default class HaAutomationTriggerRow extends LitElement {
duplicate: this._duplicateTrigger,
cut: this._cutTrigger,
insertAfter: this._insertAfter,
config: trigger,
uiSupported: this._uiSupported(
this._getType(trigger, this.triggerDescriptions)
),
description:
"trigger" in trigger
? this.triggerDescriptions[trigger.trigger]
: undefined,
config: trigger || this.trigger,
uiSupported: this._uiSupported(this._getType(trigger || this.trigger)),
yamlMode: this._yamlMode,
} satisfies TriggerSidebarConfig);
this._selected = true;
@@ -782,18 +759,8 @@ export default class HaAutomationTriggerRow extends LitElement {
});
}
private _getType = memoizeOne(
(trigger: Trigger, triggerDescriptions: TriggerDescriptions) => {
if (isTriggerList(trigger)) {
return "list";
}
if (trigger.trigger in triggerDescriptions) {
return "platform";
}
return trigger.trigger;
}
private _getType = memoizeOne((trigger: Trigger) =>
isTriggerList(trigger) ? "list" : trigger.trigger
);
private _uiSupported = memoizeOne(

View File

@@ -4,7 +4,6 @@ import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { ensureArray } from "../../../../common/array/ensure-array";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
@@ -13,16 +12,12 @@ import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import {
getValueFromDynamic,
isDynamic,
type AutomationClipboard,
type Trigger,
type TriggerList,
import type {
AutomationClipboard,
Trigger,
TriggerList,
} from "../../../../data/automation";
import type { TriggerDescriptions } from "../../../../data/trigger";
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { isTriggerList } from "../../../../data/trigger";
import type { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
@@ -31,9 +26,10 @@ import {
import { automationRowsStyles } from "../styles";
import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import { ensureArray } from "../../../../common/array/ensure-array";
@customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
export default class HaAutomationTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public triggers!: Trigger[];
@@ -66,23 +62,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
private _triggerKeys = new WeakMap<Trigger, string>();
@state() private _triggerDescriptions: TriggerDescriptions = {};
protected hassSubscribe() {
return [
subscribeTriggers(this.hass, (triggers) => this._addTriggers(triggers)),
];
}
private _addTriggers(triggers: TriggerDescriptions) {
this._triggerDescriptions = { ...this._triggerDescriptions, ...triggers };
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("triggers");
}
protected render() {
return html`
<ha-sortable
@@ -106,7 +85,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
.first=${idx === 0}
.last=${idx === this.triggers.length - 1}
.trigger=${trg}
.triggerDescriptions=${this._triggerDescriptions}
@duplicate=${this._duplicateTrigger}
@insert-after=${this._insertAfter}
@move-down=${this._moveDown}
@@ -178,10 +156,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
let triggers: Trigger[];
if (value === PASTE_VALUE) {
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
} else if (isDynamic(value)) {
triggers = this.triggers.concat({
trigger: getValueFromDynamic(value),
});
} else {
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
const elClass = customElements.get(

View File

@@ -1,416 +0,0 @@
import { mdiHelpCircle } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import "../../../../../components/ha-checkbox";
import "../../../../../components/ha-selector/ha-selector";
import "../../../../../components/ha-settings-row";
import type { PlatformTrigger } from "../../../../../data/automation";
import type { IntegrationManifest } from "../../../../../data/integration";
import { fetchIntegrationManifest } from "../../../../../data/integration";
import type { TargetSelector } from "../../../../../data/selector";
import {
getTriggerDomain,
getTriggerObjectId,
type TriggerDescription,
} from "../../../../../data/trigger";
import type { HomeAssistant } from "../../../../../types";
import { documentationUrl } from "../../../../../util/documentation-url";
const showOptionalToggle = (field: TriggerDescription["fields"][string]) =>
field.selector &&
!field.required &&
!("boolean" in field.selector && field.default);
@customElement("ha-automation-trigger-platform")
export class HaPlatformTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trigger!: PlatformTrigger;
@property({ attribute: false }) public description?: TriggerDescription;
@property({ type: Boolean }) public disabled = false;
@state() private _checkedKeys = new Set();
@state() private _manifest?: IntegrationManifest;
public static get defaultConfig(): PlatformTrigger {
return { trigger: "" };
}
protected willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (!this.hasUpdated) {
this.hass.loadBackendTranslation("triggers");
this.hass.loadBackendTranslation("selector");
}
if (!changedProperties.has("trigger")) {
return;
}
const oldValue = changedProperties.get("trigger") as
| undefined
| this["trigger"];
// Fetch the manifest if we have a trigger selected and the trigger domain changed.
// If no trigger is selected, clear the manifest.
if (this.trigger?.trigger) {
const domain = getTriggerDomain(this.trigger.trigger);
const oldDomain = getTriggerDomain(oldValue?.trigger || "");
if (domain !== oldDomain) {
this._fetchManifest(domain);
}
} else {
this._manifest = undefined;
}
}
protected render() {
const domain = getTriggerDomain(this.trigger.trigger);
const triggerName = getTriggerObjectId(this.trigger.trigger);
const description = this.hass.localize(
`component.${domain}.triggers.${triggerName}.description`
);
const triggerDesc = this.description;
const shouldRenderDataYaml = !triggerDesc?.fields;
const hasOptional = Boolean(
triggerDesc?.fields &&
Object.values(triggerDesc.fields).some((field) =>
showOptionalToggle(field)
)
);
return html`
<div class="description">
${description ? html`<p>${description}</p>` : nothing}
${this._manifest
? html`<a
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
)
: this._manifest.documentation}
title=${this.hass.localize(
"ui.components.service-control.integration_doc"
)}
target="_blank"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
class="help-icon"
></ha-icon-button>
</a>`
: nothing}
</div>
${triggerDesc && "target" in triggerDesc
? html`<ha-settings-row narrow>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: nothing}
<span slot="heading"
>${this.hass.localize(
"ui.components.service-control.target"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.components.service-control.target_secondary"
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(triggerDesc.target)}
.disabled=${this.disabled}
@value-changed=${this._targetChanged}
.value=${this.trigger?.target}
></ha-selector
></ha-settings-row>`
: nothing}
${shouldRenderDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
.name=${"data"}
.readOnly=${this.disabled}
.defaultValue=${this.trigger?.options}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: Object.entries(triggerDesc.fields).map(([fieldName, dataField]) =>
this._renderField(
fieldName,
dataField,
hasOptional,
domain,
triggerName
)
)}
`;
}
private _targetSelector = memoizeOne(
(targetSelector: TargetSelector["target"] | null | undefined) =>
targetSelector ? { target: { ...targetSelector } } : { target: {} }
);
private _renderField = (
fieldName: string,
dataField: TriggerDescription["fields"][string],
hasOptional: boolean,
domain: string | undefined,
triggerName: string | undefined
) => {
const selector = dataField?.selector ?? { text: null };
const showOptional = showOptionalToggle(dataField);
return dataField.selector
? html`<ha-settings-row narrow>
${!showOptional
? hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: nothing
: html`<ha-checkbox
.key=${fieldName}
.checked=${this._checkedKeys.has(fieldName) ||
(this.trigger?.options &&
this.trigger.options[fieldName] !== undefined)}
.disabled=${this.disabled}
@change=${this._checkboxChanged}
slot="prefix"
></ha-checkbox>`}
<span slot="heading"
>${this.hass.localize(
`component.${domain}.triggers.${triggerName}.fields.${fieldName}.name`
) || triggerName}</span
>
<span slot="description"
>${this.hass.localize(
`component.${domain}.triggers.${triggerName}.fields.${fieldName}.description`
)}</span
>
<ha-selector
.disabled=${this.disabled ||
(showOptional &&
!this._checkedKeys.has(fieldName) &&
(!this.trigger?.options ||
this.trigger.options[fieldName] === undefined))}
.hass=${this.hass}
.selector=${selector}
.context=${this._generateContext(dataField)}
.key=${fieldName}
@value-changed=${this._dataChanged}
.value=${this.trigger?.options
? this.trigger.options[fieldName]
: undefined}
.placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback}
></ha-selector>
</ha-settings-row>`
: nothing;
};
private _generateContext(
field: TriggerDescription["fields"][string]
): Record<string, any> | undefined {
if (!field.context) {
return undefined;
}
const context = {};
for (const [context_key, data_key] of Object.entries(field.context)) {
context[context_key] =
data_key === "target"
? this.trigger.target
: this.trigger.options?.[data_key];
}
return context;
}
private _dataChanged(ev: CustomEvent) {
ev.stopPropagation();
if (ev.detail.isValid === false) {
// Don't clear an object selector that returns invalid YAML
return;
}
const key = (ev.currentTarget as any).key;
const value = ev.detail.value;
if (
this.trigger?.options?.[key] === value ||
((!this.trigger?.options || !(key in this.trigger.options)) &&
(value === "" || value === undefined))
) {
return;
}
const options = { ...this.trigger?.options, [key]: value };
if (
value === "" ||
value === undefined ||
(typeof value === "object" && !Object.keys(value).length)
) {
delete options[key];
}
fireEvent(this, "value-changed", {
value: {
...this.trigger,
options,
},
});
}
private _targetChanged(ev: CustomEvent): void {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.trigger,
target: ev.detail.value,
},
});
}
private _checkboxChanged(ev) {
const checked = ev.currentTarget.checked;
const key = ev.currentTarget.key;
let options;
if (checked) {
this._checkedKeys.add(key);
const field =
this.description &&
Object.entries(this.description).find(([k, _value]) => k === key)?.[1];
let defaultValue = field?.default;
if (
defaultValue == null &&
field?.selector &&
"constant" in field.selector
) {
defaultValue = field.selector.constant?.value;
}
if (
defaultValue == null &&
field?.selector &&
"boolean" in field.selector
) {
defaultValue = false;
}
if (defaultValue != null) {
options = {
...this.trigger?.options,
[key]: defaultValue,
};
}
} else {
this._checkedKeys.delete(key);
options = { ...this.trigger?.options };
delete options[key];
}
if (options) {
fireEvent(this, "value-changed", {
value: {
...this.trigger,
options,
},
});
}
this.requestUpdate("_checkedKeys");
}
private _localizeValueCallback = (key: string) => {
if (!this.trigger?.trigger) {
return "";
}
return this.hass.localize(
`component.${computeDomain(this.trigger.trigger)}.selector.${key}`
);
};
private async _fetchManifest(integration: string) {
this._manifest = undefined;
try {
this._manifest = await fetchIntegrationManifest(this.hass, integration);
} catch (_err: any) {
// eslint-disable-next-line no-console
console.log(`Unable to fetch integration manifest for ${integration}`);
// Ignore if loading manifest fails. Probably bad JSON in manifest
}
}
static styles = css`
ha-settings-row {
padding: 0 var(--ha-space-4);
}
ha-settings-row[narrow] {
padding-bottom: var(--ha-space-2);
}
ha-settings-row {
--settings-row-content-width: 100%;
--settings-row-prefix-display: contents;
border-top: var(
--service-control-items-border-top,
1px solid var(--divider-color)
);
}
ha-service-picker,
ha-entity-picker,
ha-yaml-editor {
display: block;
margin: 0 var(--ha-space-4);
}
ha-yaml-editor {
padding: var(--ha-space-4) 0;
}
p {
margin: 0 var(--ha-space-4);
padding: var(--ha-space-4) 0;
}
:host([hide-picker]) p {
padding-top: 0;
}
.checkbox-spacer {
width: 32px;
}
ha-checkbox {
margin-left: calc(var(--ha-space-4) * -1);
margin-inline-start: calc(var(--ha-space-4) * -1);
margin-inline-end: initial;
}
.help-icon {
color: var(--secondary-text-color);
}
.description {
justify-content: space-between;
display: flex;
align-items: center;
padding-right: 2px;
padding-inline-end: 2px;
padding-inline-start: initial;
}
.description p {
direction: ltr;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-platform": HaPlatformTrigger;
}
}

View File

@@ -16,10 +16,8 @@ import {
import type {
BarSeriesOption,
CallbackDataParams,
LineSeriesOption,
TopLevelFormatterParams,
} from "echarts/types/dist/shared";
import type { LineDataItemOption } from "echarts/types/src/chart/line/LineSeries";
import type { FrontendLocaleData } from "../../../../../data/translation";
import { formatNumber } from "../../../../../common/number/format_number";
import {
@@ -172,10 +170,11 @@ function formatTooltip(
compare
? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `
: ""
}${formatTime(date, locale, config)}`;
if (params[0].componentSubType === "bar") {
period += ` ${formatTime(addHours(date, 1), locale, config)}`;
}
}${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1),
locale,
config
)}`;
}
const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`;
@@ -282,35 +281,6 @@ export function fillDataGapsAndRoundCaps(datasets: BarSeriesOption[]) {
});
}
export function fillLineGaps(datasets: LineSeriesOption[]) {
const buckets = Array.from(
new Set(
datasets
.map((dataset) =>
dataset.data!.map((datapoint) => Number(datapoint![0]))
)
.flat()
)
).sort((a, b) => a - b);
buckets.forEach((bucket, index) => {
for (let i = datasets.length - 1; i >= 0; i--) {
const dataPoint = datasets[i].data![index];
const item: LineDataItemOption =
dataPoint && typeof dataPoint === "object" && "value" in dataPoint
? dataPoint
: ({ value: dataPoint } as LineDataItemOption);
const x = item.value?.[0];
if (x === undefined) {
continue;
}
if (Number(x) !== bucket) {
datasets[i].data?.splice(index, 0, [bucket, 0]);
}
}
});
return datasets;
}
export function getCompareTransform(start: Date, compareStart?: Date) {
if (!compareStart) {
return (ts: Date) => ts;

View File

@@ -1,335 +0,0 @@
import { endOfToday, isToday, startOfToday } from "date-fns";
import type { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { LineSeriesOption } from "echarts/charts";
import { graphic } from "echarts";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import type { EnergyData } from "../../../../data/energy";
import { getEnergyDataCollection } from "../../../../data/energy";
import type { StatisticValue } from "../../../../data/recorder";
import type { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceCard } from "../../types";
import type { PowerSourcesGraphCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed";
import { getCommonOptions, fillLineGaps } from "./common/energy-chart-options";
import type { ECOption } from "../../../../resources/echarts/echarts";
import { hex2rgb } from "../../../../common/color/convert-color";
@customElement("hui-power-sources-graph-card")
export class HuiPowerSourcesGraphCard
extends SubscribeMixin(LitElement)
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: PowerSourcesGraphCardConfig;
@state() private _chartData: LineSeriesOption[] = [];
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@state() private _compareStart?: Date;
@state() private _compareEnd?: Date;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass, {
key: this._config?.collection_key,
}).subscribe((data) => this._getStatistics(data)),
];
}
public getCardSize(): Promise<number> | number {
return 3;
}
public setConfig(config: PowerSourcesGraphCardConfig): void {
this._config = config;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return (
hasConfigChanged(this, changedProps) ||
changedProps.size > 1 ||
!changedProps.has("hass")
);
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-card>
${this._config.title
? html`<h1 class="card-header">${this._config.title}</h1>`
: ""}
<div
class="content ${classMap({
"has-header": !!this._config.title,
})}"
>
<ha-chart-base
.hass=${this.hass}
.data=${this._chartData}
.options=${this._createOptions(
this._start,
this._end,
this.hass.locale,
this.hass.config,
this._compareStart,
this._compareEnd
)}
></ha-chart-base>
${!this._chartData.some((dataset) => dataset.data!.length)
? html`<div class="no-data">
${isToday(this._start)
? this.hass.localize("ui.panel.lovelace.cards.energy.no_data")
: this.hass.localize(
"ui.panel.lovelace.cards.energy.no_data_period"
)}
</div>`
: nothing}
</div>
</ha-card>
`;
}
private _createOptions = memoizeOne(
(
start: Date,
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
compareStart?: Date,
compareEnd?: Date
): ECOption =>
getCommonOptions(
start,
end,
locale,
config,
"kW",
compareStart,
compareEnd
)
);
private async _getStatistics(energyData: EnergyData): Promise<void> {
const datasets: LineSeriesOption[] = [];
const statIds = {
solar: {
stats: [] as string[],
color: "--energy-solar-color",
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.power_graph.solar"
),
},
grid: {
stats: [] as string[],
color: "--energy-grid-consumption-color",
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.power_graph.grid"
),
},
battery: {
stats: [] as string[],
color: "--energy-battery-out-color",
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.power_graph.battery"
),
},
};
const computedStyles = getComputedStyle(this);
for (const source of energyData.prefs.energy_sources) {
if (source.type === "solar") {
if (source.stat_rate) {
statIds.solar.stats.push(source.stat_rate);
}
continue;
}
if (source.type === "battery") {
if (source.stat_rate) {
statIds.battery.stats.push(source.stat_rate);
}
continue;
}
if (source.type === "grid" && source.power) {
statIds.grid.stats.push(...source.power.map((p) => p.stat_rate));
}
}
const commonSeriesOptions: LineSeriesOption = {
type: "line",
smooth: 0.4,
smoothMonotone: "x",
lineStyle: {
width: 1,
},
};
Object.keys(statIds).forEach((key, keyIndex) => {
if (statIds[key].stats.length) {
const colorHex = computedStyles.getPropertyValue(statIds[key].color);
const rgb = hex2rgb(colorHex);
// Echarts is supposed to handle that but it is bugged when you use it together with stacking.
// The interpolation breaks the stacking, so this positive/negative is a workaround
const { positive, negative } = this._processData(
statIds[key].stats.map((id: string) => energyData.stats[id] ?? [])
);
datasets.push({
...commonSeriesOptions,
id: key,
name: statIds[key].name,
color: colorHex,
stack: "positive",
areaStyle: {
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
},
{
offset: 1,
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
},
]),
},
data: positive,
z: 3 - keyIndex, // draw in reverse order so 0 value lines are overwritten
});
if (key !== "solar") {
datasets.push({
...commonSeriesOptions,
id: `${key}-negative`,
name: statIds[key].name,
color: colorHex,
stack: "negative",
areaStyle: {
color: new graphic.LinearGradient(0, 1, 0, 0, [
{
offset: 0,
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
},
{
offset: 1,
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
},
]),
},
data: negative,
z: 4 - keyIndex, // draw in reverse order but above positive series
});
}
}
});
this._start = energyData.start;
this._end = energyData.end || endOfToday();
this._chartData = fillLineGaps(datasets);
const usageData: NonNullable<LineSeriesOption["data"]> = [];
this._chartData[0]?.data!.forEach((item, i) => {
// fillLineGaps ensures all datasets have the same x values
const x =
typeof item === "object" && "value" in item!
? item.value![0]
: item![0];
usageData[i] = [x, 0];
this._chartData.forEach((dataset) => {
const y =
typeof dataset.data![i] === "object" && "value" in dataset.data![i]!
? dataset.data![i].value![1]
: dataset.data![i]![1];
usageData[i]![1] += y as number;
});
});
this._chartData.push({
...commonSeriesOptions,
id: "usage",
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.power_graph.usage"
),
color: computedStyles.getPropertyValue("--primary-color"),
lineStyle: { width: 2 },
data: usageData,
z: 5,
});
}
private _processData(stats: StatisticValue[][]) {
const data: Record<number, number[]> = {};
stats.forEach((statSet) => {
statSet.forEach((point) => {
if (point.mean == null) {
return;
}
const x = (point.start + point.end) / 2;
data[x] = [...(data[x] ?? []), point.mean];
});
});
const positive: [number, number][] = [];
const negative: [number, number][] = [];
Object.entries(data).forEach(([x, y]) => {
const ts = Number(x);
const meanY = y.reduce((a, b) => a + b, 0) / y.length;
positive.push([ts, Math.max(0, meanY)]);
negative.push([ts, Math.min(0, meanY)]);
});
return { positive, negative };
}
static styles = css`
ha-card {
height: 100%;
}
.card-header {
padding-bottom: 0;
}
.content {
padding: var(--ha-space-4);
}
.has-header {
padding-top: 0;
}
.no-data {
position: absolute;
height: 100%;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
padding: 20%;
margin-left: var(--ha-space-8);
margin-inline-start: var(--ha-space-8);
margin-inline-end: initial;
box-sizing: border-box;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-power-sources-graph-card": HuiPowerSourcesGraphCard;
}
}

View File

@@ -230,11 +230,6 @@ export interface EnergySankeyCardConfig extends EnergyCardBaseConfig {
group_by_area?: boolean;
}
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
type: "power-sources-graph";
title?: string;
}
export interface EntityFilterCardConfig extends LovelaceCardConfig {
type: "entity-filter";
entities: (EntityFilterEntityConfig | string)[];

View File

@@ -66,8 +66,6 @@ const LAZY_LOAD_TYPES = {
"energy-usage-graph": () =>
import("../cards/energy/hui-energy-usage-graph-card"),
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
"power-sources-graph": () =>
import("../cards/energy/hui-power-sources-graph-card"),
"entity-filter": () => import("../cards/hui-entity-filter-card"),
error: () => import("../cards/hui-error-card"),
"home-summary": () => import("../cards/hui-home-summary-card"),

View File

@@ -22,7 +22,6 @@ const NON_STANDARD_URLS = {
"energy-devices-graph": "energy/#devices-energy-graph",
"energy-devices-detail-graph": "energy/#detail-devices-energy-graph",
"energy-sankey": "energy/#sankey-energy-graph",
"power-sources-graph": "energy/#power-sources-graph",
};
export const getCardDocumentationURL = (

View File

@@ -109,7 +109,6 @@ export class HUIViewBackground extends LitElement {
protected willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
let applyTheme = false;
if (changedProperties.has("hass") && this.hass) {
const oldHass = changedProperties.get("hass");
if (
@@ -117,18 +116,16 @@ export class HUIViewBackground extends LitElement {
this.hass.themes !== oldHass.themes ||
this.hass.selectedTheme !== oldHass.selectedTheme
) {
applyTheme = true;
this._applyTheme();
return;
}
}
if (changedProperties.has("background")) {
applyTheme = true;
this._applyTheme();
this._fetchMedia();
}
if (changedProperties.has("resolvedImage")) {
applyTheme = true;
}
if (applyTheme) {
this._applyTheme();
}
}

View File

@@ -26,7 +26,6 @@ import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { TodoItemEditDialogParams } from "./show-dialog-todo-item-editor";
import { supportsMarkdownHelper } from "../../common/translations/markdown_support";
import { formatShortDateTimeWithConditionalYear } from "../../common/datetime/format_date_time";
@customElement("dialog-todo-item-editor")
class DialogTodoItemEditor extends LitElement {
@@ -42,8 +41,6 @@ class DialogTodoItemEditor extends LitElement {
@state() private _due?: Date;
@state() private _completedTime?: Date;
@state() private _checked = false;
@state() private _hasTime = false;
@@ -68,9 +65,6 @@ class DialogTodoItemEditor extends LitElement {
this._checked = entry.status === TodoItemStatus.Completed;
this._summary = entry.summary;
this._description = entry.description || "";
this._completedTime = entry.completed
? new Date(entry.completed)
: undefined;
this._hasTime = entry.due?.includes("T") || false;
this._due = entry.due
? new Date(this._hasTime ? entry.due : `${entry.due}T00:00:00`)
@@ -144,17 +138,6 @@ class DialogTodoItemEditor extends LitElement {
.disabled=${!canUpdate}
></ha-textfield>
</div>
${this._completedTime
? html`<div class="italic">
${this.hass.localize("ui.components.todo.item.completed_time", {
datetime: formatShortDateTimeWithConditionalYear(
this._completedTime,
this.hass.locale,
this.hass.config
),
})}
</div>`
: nothing}
${this._todoListSupportsFeature(
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
)
@@ -472,9 +455,6 @@ class DialogTodoItemEditor extends LitElement {
display: inline-block;
vertical-align: top;
}
.italic {
font-style: italic;
}
`,
];
}

View File

@@ -199,3 +199,23 @@ export const baseEntrypointStyles = css`
width: 100vw;
}
`;
export const baseAnimationStyles = css`
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
`;

View File

@@ -1,30 +0,0 @@
import { css } from "lit";
export const animationStyles = css`
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes scale {
from {
transform: scale(0);
}
to {
transform: scale(1);
}
}
`;

View File

@@ -52,6 +52,8 @@ export const waColorStyles = css`
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
--wa-color-text-quiet: var(--ha-color-text-secondary);
--wa-color-text-normal: var(--ha-color-text-primary);
--wa-color-surface-default: var(--card-background-color);
--wa-color-surface-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff));
@@ -62,5 +64,7 @@ export const waColorStyles = css`
--wa-focus-ring-color: var(--ha-color-neutral-60);
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
--wa-color-text-normal: var(--ha-color-text-primary);
}
`;

View File

@@ -55,7 +55,6 @@ export const coreStyles = css`
--ha-shadow-spread-sm: 0;
--ha-shadow-spread-md: 0;
--ha-shadow-spread-lg: 0;
--ha-animation-base-duration: 350ms;
}

View File

@@ -1,5 +1,4 @@
import { fontStyles } from "../roboto";
import { animationStyles } from "./animations.globals";
import { colorDerivedVariables, colorStylesCollection } from "./color";
import { coreDerivedVariables, coreStyles } from "./core.globals";
import { mainDerivedVariables, mainStyles } from "./main.globals";
@@ -18,7 +17,6 @@ export const themeStyles = [
...colorStylesCollection,
fontStyles.toString(),
waMainStyles.toString(),
animationStyles.toString(),
].join("");
export const derivedStyles = {

View File

@@ -9,12 +9,16 @@ export const waMainStyles = css`
--wa-focus-ring-offset: 2px;
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-focus-ring-color);
--wa-space-xs: var(--ha-space-2);
--wa-space-m: var(--ha-space-4);
--wa-space-l: var(--ha-space-6);
--wa-space-xl: var(--ha-space-8);
--wa-form-control-padding-block: 0.75em;
--wa-form-control-value-line-height: var(--ha-line-height-condensed);
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-transition-normal: 150ms;
--wa-transition-fast: 75ms;
--wa-transition-easing: ease;
@@ -28,6 +32,7 @@ export const waMainStyles = css`
--wa-line-height-condensed: var(--ha-line-height-condensed);
--wa-font-size-m: var(--ha-font-size-m);
--wa-shadow-s: var(--ha-box-shadow-s);
--wa-shadow-m: var(--ha-box-shadow-m);
--wa-shadow-l: var(--ha-box-shadow-l);

View File

@@ -1130,7 +1130,6 @@
"edit": "Edit item",
"save": "Save item",
"due": "Due date",
"completed_time": "Completed { datetime }",
"not_all_required_fields": "Not all required fields are filled in",
"confirm_delete": {
"delete": "Delete item",
@@ -3990,6 +3989,9 @@
"item_pasted": "{item} pasted",
"ctrl": "Ctrl",
"del": "Del",
"targets": "Targets",
"select_target": "Select a target",
"home": "Home",
"blocks": "Blocks",
"triggers": {
"name": "Triggers",
@@ -7162,12 +7164,6 @@
"low_carbon_energy_consumed": "Low-carbon electricity consumed",
"low_carbon_energy_not_calculated": "Consumed low-carbon electricity couldn't be calculated"
},
"power_graph": {
"grid": "Grid",
"solar": "Solar",
"battery": "Battery",
"usage": "Used"
},
"energy_compare": {
"info": "You are comparing the period {start} with the period {end}",
"compare_previous_year": "Compare previous year",

View File

@@ -1,16 +1,12 @@
import type { TemplateResult } from "lit";
import { render } from "lit";
import { parseAnimationDuration } from "../common/util/parse-animation-duration";
import { withViewTransition } from "../common/util/view-transition";
export const removeLaunchScreen = () => {
const launchScreenElement = document.getElementById("ha-launch-screen");
if (!launchScreenElement?.parentElement) {
return;
}
withViewTransition((viewTransitionAvailable: boolean) => {
if (!viewTransitionAvailable) {
const removeElement = (
launchScreenElement: HTMLElement,
skipAnimation: boolean
) => {
if (skipAnimation) {
launchScreenElement.parentElement?.removeChild(launchScreenElement);
return;
}
@@ -24,7 +20,22 @@ export const removeLaunchScreen = () => {
setTimeout(() => {
launchScreenElement.parentElement?.removeChild(launchScreenElement);
}, parseAnimationDuration(durationFromCss));
};
export const removeLaunchScreen = () => {
const launchScreenElement = document.getElementById("ha-launch-screen");
if (!launchScreenElement?.parentElement) {
return;
}
if (document.startViewTransition) {
document.startViewTransition(() => {
removeElement(launchScreenElement, false);
});
} else {
// Fallback: Direct removal without transition
removeElement(launchScreenElement, true);
}
};
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {