Compare commits

..

13 Commits

Author SHA1 Message Date
Bram Kragten
df7a36e743 Update text 2025-11-14 16:24:46 +01:00
Bram Kragten
5786fe4b8d update link 2025-11-14 16:18:33 +01:00
Bram Kragten
6fa274e4bf Add device database toggle to analytics 2025-11-14 16:10:46 +01:00
Aidan Timson
1bd1e015ff Migrate dialog-lovelace-resource-detail to ha-wa-dialog (#27939) 2025-11-14 08:32:41 +02:00
Aidan Timson
7588490419 Migrate dialog-config-entry-system-options to ha-wa-dialog (#27938) 2025-11-14 08:27:17 +02:00
Petar Petrov
2e80a3ddab Add configurable chart modes in energy devices graph card (#27937) 2025-11-14 08:16:36 +02:00
Bram Kragten
332694549c Add support for triggers.yaml (#27379) 2025-11-13 23:31:40 +01:00
karwosts
396ddef722 Expose completed timestamp for TodoItem (#27943) 2025-11-13 22:40:56 +01:00
Aidan Timson
d02804449a Merge media selectors for index.html.template (#27941) 2025-11-13 22:33:30 +01:00
Simon Lamon
4ab24cdc72 Rspack: Deprecated layers (#27942) 2025-11-13 22:32:37 +01:00
Aidan Timson
81c27090d2 Create withViewTransition wrapper function (#27918)
* Create withViewTransition wrapper function

* Add missing space

* Remove function, check for view transition, add param

* Document
2025-11-13 17:32:15 +02:00
karwosts
09bdfd3ad7 Fix incorrect (Disabled) string in trigger (#27935) 2025-11-13 14:36:05 +00:00
karwosts
97e49f751c Fix media image on dashboard-level background (#27934) 2025-11-13 15:43:27 +02:00
36 changed files with 1338 additions and 318 deletions

View File

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

View File

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

View File

@@ -153,7 +153,7 @@
"@babel/plugin-transform-runtime": "7.28.5", "@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5", "@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.6", "@bundle-stats/plugin-webpack-filter": "4.21.6",
"@lokalise/node-api": "15.4.0", "@lokalise/node-api": "15.3.1",
"@octokit/auth-oauth-device": "8.0.3", "@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3", "@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1", "@octokit/rest": "22.0.1",

View File

@@ -0,0 +1,30 @@
/**
* 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

@@ -1,6 +1,8 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import type { StateSelector } from "../../data/selector"; import type { StateSelector } from "../../data/selector";
import { extractFromTarget } from "../../data/target";
import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../entity/ha-entity-state-picker"; import "../entity/ha-entity-state-picker";
@@ -25,15 +27,29 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public context?: { @property({ attribute: false }) public context?: {
filter_attribute?: string; filter_attribute?: string;
filter_entity?: string | 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() { protected render() {
if (this.selector.state?.multiple) { if (this.selector.state?.multiple) {
return html` return html`
<ha-entity-states-picker <ha-entity-states-picker
.hass=${this.hass} .hass=${this.hass}
.entityId=${this.selector.state?.entity_id || .entityId=${this._entityIds}
this.context?.filter_entity}
.attribute=${this.selector.state?.attribute || .attribute=${this.selector.state?.attribute ||
this.context?.filter_attribute} this.context?.filter_attribute}
.extraOptions=${this.selector.state?.extra_options} .extraOptions=${this.selector.state?.extra_options}
@@ -50,8 +66,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
return html` return html`
<ha-entity-state-picker <ha-entity-state-picker
.hass=${this.hass} .hass=${this.hass}
.entityId=${this.selector.state?.entity_id || .entityId=${this._entityIds}
this.context?.filter_entity}
.attribute=${this.selector.state?.attribute || .attribute=${this.selector.state?.attribute ||
this.context?.filter_attribute} this.context?.filter_attribute}
.extraOptions=${this.selector.state?.extra_options} .extraOptions=${this.selector.state?.extra_options}
@@ -65,6 +80,24 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
></ha-entity-state-picker> ></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 { declare global {

View File

@@ -0,0 +1,97 @@
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: { groups: {
device_id: {}, device_id: {},
serviceGroups: {}, dynamicGroups: {},
}, },
}, },
{ {
@@ -117,14 +117,6 @@ export const VIRTUAL_ACTIONS: Partial<
}, },
} as const; } 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 = [ export const COLLAPSIBLE_ACTION_ELEMENTS = [
"ha-automation-action-choose", "ha-automation-action-choose",
"ha-automation-action-condition", "ha-automation-action-condition",

View File

@@ -5,6 +5,7 @@ export interface AnalyticsPreferences {
diagnostics?: boolean; diagnostics?: boolean;
usage?: boolean; usage?: boolean;
statistics?: boolean; statistics?: boolean;
snapshots?: boolean;
} }
export interface Analytics { export interface Analytics {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,57 +1,20 @@
import { import { mdiMapClock, mdiShape } from "@mdi/js";
mdiAvTimer,
mdiCalendar,
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiMapClock,
mdiMapMarker,
mdiMapMarkerRadius,
mdiMessageAlert,
mdiMicrophoneMessage,
mdiNfcVariant,
mdiNumeric,
mdiShape,
mdiStateMachine,
mdiSwapHorizontal,
mdiWeatherSunny,
mdiWebhook,
} from "@mdi/js";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg"; import { computeDomain } from "../common/entity/compute_domain";
import { computeObjectId } from "../common/entity/compute_object_id";
import type { HomeAssistant } from "../types";
import type { import type {
AutomationElementGroupCollection, AutomationElementGroupCollection,
Trigger, Trigger,
TriggerList, TriggerList,
} from "./automation"; } 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[] = [ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
{ {
groups: { groups: {
device: {}, device: {},
dynamicGroups: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } }, entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: { time_location: {
icon: mdiMapClock, icon: mdiMapClock,
@@ -83,3 +46,33 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
export const isTriggerList = (trigger: Trigger): trigger is TriggerList => export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
"triggers" in trigger; "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

@@ -2,7 +2,8 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { createCloseHeading } from "../../components/ha-dialog"; import "../../components/ha-wa-dialog";
import "../../components/ha-dialog-footer";
import "../../components/ha-formfield"; import "../../components/ha-formfield";
import "../../components/ha-switch"; import "../../components/ha-switch";
import "../../components/ha-button"; import "../../components/ha-button";
@@ -28,6 +29,8 @@ class DialogConfigEntrySystemOptions extends LitElement {
@state() private _submitting = false; @state() private _submitting = false;
@state() private _open = false;
public async showDialog( public async showDialog(
params: ConfigEntrySystemOptionsDialogParams params: ConfigEntrySystemOptionsDialogParams
): Promise<void> { ): Promise<void> {
@@ -35,9 +38,14 @@ class DialogConfigEntrySystemOptions extends LitElement {
this._error = undefined; this._error = undefined;
this._disableNewEntities = params.entry.pref_disable_new_entities; this._disableNewEntities = params.entry.pref_disable_new_entities;
this._disablePolling = params.entry.pref_disable_polling; this._disablePolling = params.entry.pref_disable_polling;
this._open = true;
} }
public closeDialog(): void { public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = ""; this._error = "";
this._params = undefined; this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -49,18 +57,19 @@ class DialogConfigEntrySystemOptions extends LitElement {
} }
return html` return html`
<ha-dialog <ha-wa-dialog
open .hass=${this.hass}
@closed=${this.closeDialog} .open=${this._open}
.heading=${createCloseHeading( header-title=${this.hass.localize(
this.hass, "ui.dialogs.config_entry_system_options.title",
this.hass.localize("ui.dialogs.config_entry_system_options.title", { {
integration: integration:
this.hass.localize( this.hass.localize(
`component.${this._params.entry.domain}.title` `component.${this._params.entry.domain}.title`
) || this._params.entry.domain, ) || this._params.entry.domain,
}) }
)} )}
@closed=${this._dialogClosed}
> >
${this._error ? html` <div class="error">${this._error}</div> ` : ""} ${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<ha-formfield <ha-formfield
@@ -82,10 +91,10 @@ class DialogConfigEntrySystemOptions extends LitElement {
</p>`} </p>`}
> >
<ha-switch <ha-switch
autofocus
.checked=${!this._disableNewEntities} .checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged} @change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting} .disabled=${this._submitting}
dialogInitialFocus
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
@@ -113,22 +122,27 @@ class DialogConfigEntrySystemOptions extends LitElement {
.disabled=${this._submitting} .disabled=${this._submitting}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-button
appearance="plain" <ha-dialog-footer slot="footer">
slot="primaryAction" <ha-button
@click=${this.closeDialog} appearance="plain"
.disabled=${this._submitting} slot="secondaryAction"
> @click=${this.closeDialog}
${this.hass.localize("ui.common.cancel")} .disabled=${this._submitting}
</ha-button> >
<ha-button ${this.hass.localize("ui.common.cancel")}
slot="primaryAction" </ha-button>
@click=${this._updateEntry} <ha-button
.disabled=${this._submitting} slot="primaryAction"
> @click=${this._updateEntry}
${this.hass.localize("ui.dialogs.config_entry_system_options.update")} .disabled=${this._submitting}
</ha-button> >
</ha-dialog> ${this.hass.localize(
"ui.dialogs.config_entry_system_options.update"
)}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`; `;
} }

View File

@@ -40,12 +40,6 @@
color: var(--primary-text-color, #212121); color: var(--primary-text-color, #212121);
height: 100vh; height: 100vh;
} }
@media (prefers-color-scheme: dark) {
html {
background-color: var(--primary-background-color, #111111);
color: var(--primary-text-color, #e1e1e1);
}
}
#ha-launch-screen { #ha-launch-screen {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -62,12 +56,6 @@
background-color: var(--primary-background-color, #fafafa); background-color: var(--primary-background-color, #fafafa);
z-index: 100; 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 { #ha-launch-screen.removing {
opacity: 0; opacity: 0;
} }
@@ -92,6 +80,14 @@
opacity: .66; opacity: .66;
} }
@media (prefers-color-scheme: dark) { @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 { .ohf-logo {
filter: invert(1); filter: invert(1);
} }

View File

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

View File

@@ -5,6 +5,7 @@ import {
mdiPlus, mdiPlus,
} from "@mdi/js"; } from "@mdi/js";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { import {
@@ -40,32 +41,39 @@ import "../../../components/ha-md-list";
import type { HaMdList } from "../../../components/ha-md-list"; import type { HaMdList } from "../../../components/ha-md-list";
import "../../../components/ha-md-list-item"; import "../../../components/ha-md-list-item";
import "../../../components/ha-service-icon"; import "../../../components/ha-service-icon";
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
import "../../../components/ha-wa-dialog"; import "../../../components/ha-wa-dialog";
import "../../../components/search-input"; import "../../../components/search-input";
import { import {
ACTION_BUILDING_BLOCKS_GROUP, ACTION_BUILDING_BLOCKS_GROUP,
ACTION_COLLECTIONS, ACTION_COLLECTIONS,
ACTION_ICONS, ACTION_ICONS,
SERVICE_PREFIX,
getService,
isService,
} from "../../../data/action"; } from "../../../data/action";
import type { import {
AutomationElementGroup, DYNAMIC_PREFIX,
AutomationElementGroupCollection, getValueFromDynamic,
isDynamic,
type AutomationElementGroup,
type AutomationElementGroupCollection,
} from "../../../data/automation"; } from "../../../data/automation";
import { import {
CONDITION_BUILDING_BLOCKS_GROUP, CONDITION_BUILDING_BLOCKS_GROUP,
CONDITION_COLLECTIONS, CONDITION_COLLECTIONS,
CONDITION_ICONS, CONDITION_ICONS,
} from "../../../data/condition"; } from "../../../data/condition";
import { getServiceIcons } from "../../../data/icons"; import { getServiceIcons, getTriggerIcons } from "../../../data/icons";
import type { IntegrationManifest } from "../../../data/integration"; import type { IntegrationManifest } from "../../../data/integration";
import { import {
domainToName, domainToName,
fetchIntegrationManifests, fetchIntegrationManifests,
} from "../../../data/integration"; } from "../../../data/integration";
import { TRIGGER_COLLECTIONS, TRIGGER_ICONS } from "../../../data/trigger"; import type { TriggerDescriptions } from "../../../data/trigger";
import {
TRIGGER_COLLECTIONS,
getTriggerDomain,
getTriggerObjectId,
subscribeTriggers,
} from "../../../data/trigger";
import type { HassDialog } from "../../../dialogs/make-dialog-manager"; import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { HaFuse } from "../../../resources/fuse"; import { HaFuse } from "../../../resources/fuse";
@@ -111,7 +119,7 @@ const ENTITY_DOMAINS_OTHER = new Set([
const ENTITY_DOMAINS_MAIN = new Set(["notify"]); const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
const ACTION_SERVICE_KEYWORDS = ["serviceGroups", "helpers", "other"]; const ACTION_SERVICE_KEYWORDS = ["dynamicGroups", "helpers", "other"];
@customElement("add-automation-element-dialog") @customElement("add-automation-element-dialog")
class DialogAddAutomationElement class DialogAddAutomationElement
@@ -142,6 +150,8 @@ class DialogAddAutomationElement
@state() private _narrow = false; @state() private _narrow = false;
@state() private _triggerDescriptions: TriggerDescriptions = {};
@query(".items ha-md-list ha-md-list-item") @query(".items ha-md-list ha-md-list-item")
private _itemsListFirstElement?: HaMdList; private _itemsListFirstElement?: HaMdList;
@@ -152,6 +162,8 @@ class DialogAddAutomationElement
private _removeKeyboardShortcuts?: () => void; private _removeKeyboardShortcuts?: () => void;
private _unsub?: Promise<UnsubscribeFunc>;
public showDialog(params): void { public showDialog(params): void {
this._params = params; this._params = params;
@@ -163,6 +175,17 @@ class DialogAddAutomationElement
this._calculateUsedDomains(); this._calculateUsedDomains();
getServiceIcons(this.hass); 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( this._fullScreen = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)" "all and (max-width: 450px), all and (max-height: 500px)"
).matches; ).matches;
@@ -176,6 +199,10 @@ class DialogAddAutomationElement
public closeDialog() { public closeDialog() {
this.removeKeyboardShortcuts(); this.removeKeyboardShortcuts();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
if (this._params) { if (this._params) {
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@@ -317,6 +344,11 @@ class DialogAddAutomationElement
); );
const items = flattenGroups(groups).flat(); const items = flattenGroups(groups).flat();
if (type === "trigger") {
items.push(
...this._triggers(localize, this._triggerDescriptions, manifests)
);
}
if (type === "action") { if (type === "action") {
items.push(...this._services(localize, services, manifests)); items.push(...this._services(localize, services, manifests));
} }
@@ -339,6 +371,7 @@ class DialogAddAutomationElement
domains: Set<string> | undefined, domains: Set<string> | undefined,
localize: LocalizeFunc, localize: LocalizeFunc,
services: HomeAssistant["services"], services: HomeAssistant["services"],
triggerDescriptions: TriggerDescriptions,
manifests?: DomainManifestLookup manifests?: DomainManifestLookup
): { ): {
titleKey?: LocalizeKeys; titleKey?: LocalizeKeys;
@@ -362,7 +395,32 @@ class DialogAddAutomationElement
services, services,
manifests, manifests,
domains, domains,
collection.groups.serviceGroups 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
? undefined ? undefined
: collection.groups.helpers : collection.groups.helpers
? "helper" ? "helper"
@@ -429,10 +487,19 @@ class DialogAddAutomationElement
services: HomeAssistant["services"], services: HomeAssistant["services"],
manifests?: DomainManifestLookup manifests?: DomainManifestLookup
): ListItem[] => { ): ListItem[] => {
if (type === "action" && isService(group)) { if (type === "action" && isDynamic(group)) {
return this._services(localize, services, manifests, 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 groups = this._getGroups(type, group, collectionIndex);
const result = Object.entries(groups).map(([key, options]) => const result = Object.entries(groups).map(([key, options]) =>
@@ -514,7 +581,7 @@ class DialogAddAutomationElement
brand-fallback brand-fallback
></ha-domain-icon> ></ha-domain-icon>
`, `,
key: `${SERVICE_PREFIX}${domain}`, key: `${DYNAMIC_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest), name: domainToName(localize, domain, manifest),
description: "", description: "",
}); });
@@ -525,6 +592,102 @@ 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( private _services = memoizeOne(
( (
localize: LocalizeFunc, localize: LocalizeFunc,
@@ -539,8 +702,8 @@ class DialogAddAutomationElement
let domain: string | undefined; let domain: string | undefined;
if (isService(group)) { if (isDynamic(group)) {
domain = getService(group!); domain = getValueFromDynamic(group!);
} }
const addDomain = (dmn: string) => { const addDomain = (dmn: string) => {
@@ -554,7 +717,7 @@ class DialogAddAutomationElement
.service=${`${dmn}.${service}`} .service=${`${dmn}.${service}`}
></ha-service-icon> ></ha-service-icon>
`, `,
key: `${SERVICE_PREFIX}${dmn}.${service}`, key: `${DYNAMIC_PREFIX}${dmn}.${service}`,
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${ name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
this.hass.localize(`component.${dmn}.services.${service}.name`) || this.hass.localize(`component.${dmn}.services.${service}.name`) ||
services[dmn][service]?.name || services[dmn][service]?.name ||
@@ -668,14 +831,15 @@ class DialogAddAutomationElement
this._domains, this._domains,
this.hass.localize, this.hass.localize,
this.hass.services, this.hass.services,
this._triggerDescriptions,
this._manifests this._manifests
); );
const groupName = isService(this._selectedGroup) const groupName = isDynamic(this._selectedGroup)
? domainToName( ? domainToName(
this.hass.localize, this.hass.localize,
getService(this._selectedGroup!), getValueFromDynamic(this._selectedGroup!),
this._manifests?.[getService(this._selectedGroup!)] this._manifests?.[getValueFromDynamic(this._selectedGroup!)]
) )
: this.hass.localize( : this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys `ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys

View File

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

View File

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

View File

@@ -6,6 +6,9 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators"; 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 { OptionSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac"; import { isMac } from "../../../../util/is_mac";

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,416 @@
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

@@ -1,6 +1,5 @@
import { mdiOpenInNew } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-analytics"; import "../../../components/ha-analytics";
@@ -17,6 +16,8 @@ import {
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { isDevVersion } from "../../../common/config/version";
import type { HaSwitch } from "../../../components/ha-switch";
@customElement("ha-config-analytics") @customElement("ha-config-analytics")
class ConfigAnalytics extends LitElement { class ConfigAnalytics extends LitElement {
@@ -34,10 +35,22 @@ class ConfigAnalytics extends LitElement {
: undefined; : undefined;
return html` return html`
<ha-card outlined> <ha-card
outlined
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
"Home Assistant analytics"}
>
<div class="card-content"> <div class="card-content">
${error ? html`<div class="error">${error}</div>` : ""} ${error ? html`<div class="error">${error}</div>` : nothing}
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p> <p>
${this.hass.localize("ui.panel.config.analytics.intro")}
<a
href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.panel.config.analytics.learn_more")} </a
>.
</p>
<ha-analytics <ha-analytics
translation_key_panel="config" translation_key_panel="config"
@analytics-preferences-changed=${this._preferencesChanged} @analytics-preferences-changed=${this._preferencesChanged}
@@ -45,26 +58,50 @@ class ConfigAnalytics extends LitElement {
.analytics=${this._analyticsDetails} .analytics=${this._analyticsDetails}
></ha-analytics> ></ha-analytics>
</div> </div>
<div class="card-actions">
<ha-button @click=${this._save}>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.save_button"
)}
</ha-button>
</div>
</ha-card> </ha-card>
<div class="footer"> ${isDevVersion(this.hass.config.version)
<ha-button ? html`<ha-card
size="small" outlined
appearance="plain" .header=${this.hass.localize(
href=${documentationUrl(this.hass, "/integrations/analytics/")} "ui.panel.config.analytics.preferences.snapshots.header"
target="_blank" )}
rel="noreferrer" >
> <div class="card-content">
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon> <p>
${this.hass.localize("ui.panel.config.analytics.learn_more")} ${this.hass.localize(
</ha-button> "ui.panel.config.analytics.preferences.snapshots.info"
</div> )}
<a
href=${documentationUrl(this.hass, "/device-database/")}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.learn_more"
)} </a
>.
</p>
<ha-settings-row>
<span slot="heading" data-for="snapshots">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.title`
)}
</span>
<span slot="description" data-for="snapshots">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.description`
)}
</span>
<ha-switch
@change=${this._handleDeviceRowClick}
.checked=${!!this._analyticsDetails?.preferences.snapshots}
.disabled=${this._analyticsDetails === undefined}
name="snapshots"
>
</ha-switch>
</ha-settings-row>
</div>
</ha-card>`
: nothing}
`; `;
} }
@@ -96,11 +133,25 @@ class ConfigAnalytics extends LitElement {
} }
} }
private _handleDeviceRowClick(ev: Event) {
const target = ev.target as HaSwitch;
this._analyticsDetails = {
...this._analyticsDetails!,
preferences: {
...this._analyticsDetails!.preferences,
snapshots: target.checked,
},
};
this._save();
}
private _preferencesChanged(event: CustomEvent): void { private _preferencesChanged(event: CustomEvent): void {
this._analyticsDetails = { this._analyticsDetails = {
...this._analyticsDetails!, ...this._analyticsDetails!,
preferences: event.detail.preferences, preferences: event.detail.preferences,
}; };
this._save();
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -127,6 +178,9 @@ class ConfigAnalytics extends LitElement {
padding: 32px 0 16px; padding: 32px 0 16px;
text-align: center; text-align: center;
} }
ha-card:not(:first-of-type) {
margin-top: 24px;
}
ha-button[size="small"] ha-svg-icon { ha-button[size="small"] ha-svg-icon {
--mdc-icon-size: 16px; --mdc-icon-size: 16px;

View File

@@ -1,13 +1,11 @@
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { mdiClose } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-md-dialog"; import "../../../../components/ha-wa-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog"; import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog-header"; import "../../../../components/ha-alert";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource"; import type { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource";
@@ -43,7 +41,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
@state() private _submitting = false; @state() private _submitting = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog; @state() private _open = false;
public showDialog(params: LovelaceResourceDetailsDialogParams): void { public showDialog(params: LovelaceResourceDetailsDialogParams): void {
this._params = params; this._params = params;
@@ -58,6 +56,11 @@ export class DialogLovelaceResourceDetail extends LitElement {
url: "", url: "",
}; };
} }
this._open = true;
}
public closeDialog(): void {
this._open = false;
} }
private _dialogClosed(): void { private _dialogClosed(): void {
@@ -65,10 +68,6 @@ export class DialogLovelaceResourceDetail extends LitElement {
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
public closeDialog(): void {
this._dialog?.close();
}
protected render() { protected render() {
if (!this._params) { if (!this._params) {
return nothing; return nothing;
@@ -81,56 +80,45 @@ export class DialogLovelaceResourceDetail extends LitElement {
"ui.panel.config.lovelace.resources.detail.new_resource" "ui.panel.config.lovelace.resources.detail.new_resource"
); );
const ariaLabel = this._params.resource?.url
? this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.edit_resource"
)
: this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.new_resource"
);
return html` return html`
<ha-md-dialog <ha-wa-dialog
open .hass=${this.hass}
disable-cancel-action .open=${this._open}
prevent-scrim-close
header-title=${dialogTitle}
@closed=${this._dialogClosed} @closed=${this._dialogClosed}
.ariaLabel=${ariaLabel}
> >
<ha-dialog-header slot="headline"> <ha-alert
<ha-icon-button alert-type="warning"
slot="navigationIcon" .title=${this.hass!.localize(
.label=${this.hass.localize("ui.common.close") ?? "Close"} "ui.panel.config.lovelace.resources.detail.warning_header"
.path=${mdiClose} )}
@click=${this.closeDialog} >
></ha-icon-button> ${this.hass!.localize(
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span> "ui.panel.config.lovelace.resources.detail.warning_text"
</ha-dialog-header> )}
<div slot="content"> </ha-alert>
<ha-alert
alert-type="warning"
.title=${this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.warning_header"
)}
>
${this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.warning_text"
)}
</ha-alert>
<ha-form <ha-form
.schema=${this._schema(this._data)} autofocus
.data=${this._data} .schema=${this._schema(this._data)}
.hass=${this.hass} .data=${this._data}
.error=${this._error} .hass=${this.hass}
.computeLabel=${this._computeLabel} .error=${this._error}
@value-changed=${this._valueChanged} .computeLabel=${this._computeLabel}
></ha-form> @value-changed=${this._valueChanged}
</div> ></ha-form>
<div slot="actions">
<ha-button appearance="plain" @click=${this.closeDialog}> <ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass!.localize("ui.common.cancel")} ${this.hass!.localize("ui.common.cancel")}
</ha-button> </ha-button>
<ha-button <ha-button
slot="primaryAction"
@click=${this._updateResource} @click=${this._updateResource}
.disabled=${urlInvalid || !this._data?.res_type || this._submitting} .disabled=${urlInvalid || !this._data?.res_type || this._submitting}
> >
@@ -142,8 +130,8 @@ export class DialogLovelaceResourceDetail extends LitElement {
"ui.panel.config.lovelace.resources.detail.create" "ui.panel.config.lovelace.resources.detail.create"
)} )}
</ha-button> </ha-button>
</div> </ha-dialog-footer>
</ha-md-dialog> </ha-wa-dialog>
`; `;
} }

View File

@@ -60,7 +60,7 @@ export class HuiEnergyDevicesGraphCard
state: true, state: true,
subscribe: false, subscribe: false,
}) })
private _chartType: "bar" | "pie" = "bar"; private _chartType?: "bar" | "pie";
@state() @state()
@storage({ @storage({
@@ -101,6 +101,14 @@ export class HuiEnergyDevicesGraphCard
this._config = config; this._config = config;
} }
private _getAllowedModes(): ("bar" | "pie")[] {
// Empty array or undefined = allow all modes
if (!this._config?.modes || this._config.modes.length === 0) {
return ["bar", "pie"];
}
return this._config.modes;
}
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
return ( return (
hasConfigChanged(this, changedProps) || hasConfigChanged(this, changedProps) ||
@@ -109,8 +117,21 @@ export class HuiEnergyDevicesGraphCard
); );
} }
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("_config") && this._config) {
const allowedModes = this._getAllowedModes();
// If _chartType is not set or not in allowed modes, use first from config
if (!this._chartType || !allowedModes.includes(this._chartType)) {
this._chartType = allowedModes[0];
}
}
}
protected render() { protected render() {
if (!this.hass || !this._config) { if (!this.hass || !this._config || !this._chartType) {
return nothing; return nothing;
} }
@@ -118,13 +139,19 @@ export class HuiEnergyDevicesGraphCard
<ha-card> <ha-card>
<div class="card-header"> <div class="card-header">
<span>${this._config.title ? this._config.title : nothing}</span> <span>${this._config.title ? this._config.title : nothing}</span>
<ha-icon-button ${this._getAllowedModes().length > 1
.path=${this._chartType === "pie" ? mdiChartBar : mdiChartDonut} ? html`
.label=${this.hass.localize( <ha-icon-button
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type" .path=${this._chartType === "pie"
)} ? mdiChartBar
@click=${this._handleChartTypeChange} : mdiChartDonut}
></ha-icon-button> .label=${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
)}
@click=${this._handleChartTypeChange}
></ha-icon-button>
`
: nothing}
</div> </div>
<div <div
class="content ${classMap({ class="content ${classMap({
@@ -529,7 +556,13 @@ export class HuiEnergyDevicesGraphCard
} }
private _handleChartTypeChange(): void { private _handleChartTypeChange(): void {
this._chartType = this._chartType === "pie" ? "bar" : "pie"; if (!this._chartType) {
return;
}
const allowedModes = this._getAllowedModes();
const currentIndex = allowedModes.indexOf(this._chartType);
const nextIndex = (currentIndex + 1) % allowedModes.length;
this._chartType = allowedModes[nextIndex];
this._getStatistics(this._data!); this._getStatistics(this._data!);
} }

View File

@@ -185,6 +185,7 @@ export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig {
title?: string; title?: string;
max_devices?: number; max_devices?: number;
hide_compound_stats?: boolean; hide_compound_stats?: boolean;
modes?: ("bar" | "pie")[];
} }
export interface EnergyDevicesDetailGraphCardConfig export interface EnergyDevicesDetailGraphCardConfig

View File

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

View File

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

View File

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

View File

@@ -1130,6 +1130,7 @@
"edit": "Edit item", "edit": "Edit item",
"save": "Save item", "save": "Save item",
"due": "Due date", "due": "Due date",
"completed_time": "Completed { datetime }",
"not_all_required_fields": "Not all required fields are filled in", "not_all_required_fields": "Not all required fields are filled in",
"confirm_delete": { "confirm_delete": {
"delete": "Delete item", "delete": "Delete item",
@@ -6759,6 +6760,7 @@
}, },
"analytics": { "analytics": {
"caption": "Analytics", "caption": "Analytics",
"header": "Home Assistant analytics",
"description": "Learn how to share data to improve Home Assistant", "description": "Learn how to share data to improve Home Assistant",
"preferences": { "preferences": {
"base": { "base": {
@@ -6776,10 +6778,17 @@
"diagnostics": { "diagnostics": {
"title": "Diagnostics", "title": "Diagnostics",
"description": "Share crash reports when unexpected errors occur." "description": "Share crash reports when unexpected errors occur."
},
"snapshots": {
"title": "Devices",
"description": "Generic information of your devices.",
"header": "Device analytics",
"info": "Anonymously share data about your devices to help build the Open Home Foundations device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).",
"learn_more": "Learn more about the device database and how we process your data"
} }
}, },
"need_base_enabled": "You need to enable basic analytics for this option to be available", "need_base_enabled": "You need to enable basic analytics for this option to be available",
"learn_more": "How we process your data", "learn_more": "Learn how we process your data",
"intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.", "intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.",
"download_device_info": "Preview device analytics" "download_device_info": "Preview device analytics"
}, },

View File

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

View File

@@ -2365,10 +2365,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@lokalise/node-api@npm:15.4.0": "@lokalise/node-api@npm:15.3.1":
version: 15.4.0 version: 15.3.1
resolution: "@lokalise/node-api@npm:15.4.0" resolution: "@lokalise/node-api@npm:15.3.1"
checksum: 10/fe7e36bb137310244079fba9978a10fdf65ca6566e075e8e25ed0fd461e7168649ca43929a0a3a0eaf2df72055996ef4d8a72302e7b502863feb9f9a6471aff1 checksum: 10/9175559660cfbde3f6451ee0ade96ca5ccf6686f3a8f07a23ae6abf3a58db5b5dc71683cdb7f19252765250df7b77dc67539a80e24c3b44a1a97bb2f2d9cd090
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9233,7 +9233,7 @@ __metadata:
"@lit-labs/virtualizer": "npm:2.1.1" "@lit-labs/virtualizer": "npm:2.1.1"
"@lit/context": "npm:1.1.6" "@lit/context": "npm:1.1.6"
"@lit/reactive-element": "npm:2.1.1" "@lit/reactive-element": "npm:2.1.1"
"@lokalise/node-api": "npm:15.4.0" "@lokalise/node-api": "npm:15.3.1"
"@material/chips": "npm:=14.0.0-canary.53b3cad2f.0" "@material/chips": "npm:=14.0.0-canary.53b3cad2f.0"
"@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0" "@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0"
"@material/mwc-base": "npm:0.27.0" "@material/mwc-base": "npm:0.27.0"