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: {
layers: true,
outputModule: true,
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,57 +1,20 @@
import {
mdiAvTimer,
mdiCalendar,
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiMapClock,
mdiMapMarker,
mdiMapMarkerRadius,
mdiMessageAlert,
mdiMicrophoneMessage,
mdiNfcVariant,
mdiNumeric,
mdiShape,
mdiStateMachine,
mdiSwapHorizontal,
mdiWeatherSunny,
mdiWebhook,
} from "@mdi/js";
import { mdiMapClock, mdiShape } 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 {
AutomationElementGroupCollection,
Trigger,
TriggerList,
} from "./automation";
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,
};
import type { Selector, TargetSelector } from "./selector";
export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
{
groups: {
device: {},
dynamicGroups: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
@@ -83,3 +46,33 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
"triggers" in trigger;
export interface TriggerDescription {
target?: TargetSelector["target"];
fields: Record<
string,
{
example?: string | boolean | number;
default?: unknown;
required?: boolean;
selector?: Selector;
context?: Record<string, string>;
}
>;
}
export type TriggerDescriptions = Record<string, TriggerDescription>;
export const subscribeTriggers = (
hass: HomeAssistant,
callback: (triggers: TriggerDescriptions) => void
) =>
hass.connection.subscribeMessage<TriggerDescriptions>(callback, {
type: "trigger_platforms/subscribe",
});
export const getTriggerDomain = (trigger: string) =>
trigger.includes(".") ? computeDomain(trigger) : trigger;
export const getTriggerObjectId = (trigger: string) =>
trigger.includes(".") ? computeObjectId(trigger) : "_";

View File

@@ -2,7 +2,8 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
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-switch";
import "../../components/ha-button";
@@ -28,6 +29,8 @@ class DialogConfigEntrySystemOptions extends LitElement {
@state() private _submitting = false;
@state() private _open = false;
public async showDialog(
params: ConfigEntrySystemOptionsDialogParams
): Promise<void> {
@@ -35,9 +38,14 @@ class DialogConfigEntrySystemOptions extends LitElement {
this._error = undefined;
this._disableNewEntities = params.entry.pref_disable_new_entities;
this._disablePolling = params.entry.pref_disable_polling;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -49,18 +57,19 @@ class DialogConfigEntrySystemOptions extends LitElement {
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.config_entry_system_options.title", {
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
"ui.dialogs.config_entry_system_options.title",
{
integration:
this.hass.localize(
`component.${this._params.entry.domain}.title`
) || this._params.entry.domain,
})
}
)}
@closed=${this._dialogClosed}
>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<ha-formfield
@@ -82,10 +91,10 @@ class DialogConfigEntrySystemOptions extends LitElement {
</p>`}
>
<ha-switch
autofocus
.checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting}
dialogInitialFocus
></ha-switch>
</ha-formfield>
@@ -113,22 +122,27 @@ class DialogConfigEntrySystemOptions extends LitElement {
.disabled=${this._submitting}
></ha-switch>
</ha-formfield>
<ha-button
appearance="plain"
slot="primaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${this._submitting}
>
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${this._submitting}
>
${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);
height: 100vh;
}
@media (prefers-color-scheme: dark) {
html {
background-color: var(--primary-background-color, #111111);
color: var(--primary-text-color, #e1e1e1);
}
}
#ha-launch-screen {
position: fixed;
top: 0;
@@ -62,12 +56,6 @@
background-color: var(--primary-background-color, #fafafa);
z-index: 100;
}
@media (prefers-color-scheme: dark) {
/* body selector to avoid minification causing bad jinja2 */
body #ha-launch-screen {
background-color: var(--primary-background-color, #111111);
}
}
#ha-launch-screen.removing {
opacity: 0;
}
@@ -92,6 +80,14 @@
opacity: .66;
}
@media (prefers-color-scheme: dark) {
html {
background-color: var(--primary-background-color, #111111);
color: var(--primary-text-color, #e1e1e1);
}
/* body selector to avoid minification causing bad jinja2 */
body #ha-launch-screen {
background-color: var(--primary-background-color, #111111);
}
.ohf-logo {
filter: invert(1);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-analytics";
@@ -17,6 +16,8 @@ import {
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { isDevVersion } from "../../../common/config/version";
import type { HaSwitch } from "../../../components/ha-switch";
@customElement("ha-config-analytics")
class ConfigAnalytics extends LitElement {
@@ -34,10 +35,22 @@ class ConfigAnalytics extends LitElement {
: undefined;
return html`
<ha-card outlined>
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
"Home Assistant analytics"}
>
<div class="card-content">
${error ? html`<div class="error">${error}</div>` : ""}
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p>
${error ? html`<div class="error">${error}</div>` : nothing}
<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
translation_key_panel="config"
@analytics-preferences-changed=${this._preferencesChanged}
@@ -45,26 +58,50 @@ class ConfigAnalytics extends LitElement {
.analytics=${this._analyticsDetails}
></ha-analytics>
</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>
<div class="footer">
<ha-button
size="small"
appearance="plain"
href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
${this.hass.localize("ui.panel.config.analytics.learn_more")}
</ha-button>
</div>
${isDevVersion(this.hass.config.version)
? html`<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.header"
)}
>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.info"
)}
<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 {
this._analyticsDetails = {
...this._analyticsDetails!,
preferences: event.detail.preferences,
};
this._save();
}
static get styles(): CSSResultGroup {
@@ -127,6 +178,9 @@ class ConfigAnalytics extends LitElement {
padding: 32px 0 16px;
text-align: center;
}
ha-card:not(:first-of-type) {
margin-top: 24px;
}
ha-button[size="small"] ha-svg-icon {
--mdc-icon-size: 16px;

View File

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

View File

@@ -60,7 +60,7 @@ export class HuiEnergyDevicesGraphCard
state: true,
subscribe: false,
})
private _chartType: "bar" | "pie" = "bar";
private _chartType?: "bar" | "pie";
@state()
@storage({
@@ -101,6 +101,14 @@ export class HuiEnergyDevicesGraphCard
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 {
return (
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() {
if (!this.hass || !this._config) {
if (!this.hass || !this._config || !this._chartType) {
return nothing;
}
@@ -118,13 +139,19 @@ export class HuiEnergyDevicesGraphCard
<ha-card>
<div class="card-header">
<span>${this._config.title ? this._config.title : nothing}</span>
<ha-icon-button
.path=${this._chartType === "pie" ? mdiChartBar : mdiChartDonut}
.label=${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
)}
@click=${this._handleChartTypeChange}
></ha-icon-button>
${this._getAllowedModes().length > 1
? html`
<ha-icon-button
.path=${this._chartType === "pie"
? mdiChartBar
: mdiChartDonut}
.label=${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
)}
@click=${this._handleChartTypeChange}
></ha-icon-button>
`
: nothing}
</div>
<div
class="content ${classMap({
@@ -529,7 +556,13 @@ export class HuiEnergyDevicesGraphCard
}
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!);
}

View File

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

View File

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

View File

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

View File

@@ -1130,6 +1130,7 @@
"edit": "Edit item",
"save": "Save item",
"due": "Due date",
"completed_time": "Completed { datetime }",
"not_all_required_fields": "Not all required fields are filled in",
"confirm_delete": {
"delete": "Delete item",
@@ -6759,6 +6760,7 @@
},
"analytics": {
"caption": "Analytics",
"header": "Home Assistant analytics",
"description": "Learn how to share data to improve Home Assistant",
"preferences": {
"base": {
@@ -6776,10 +6778,17 @@
"diagnostics": {
"title": "Diagnostics",
"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",
"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.",
"download_device_info": "Preview device analytics"
},

View File

@@ -1,26 +1,7 @@
import type { TemplateResult } from "lit";
import { render } from "lit";
import { parseAnimationDuration } from "../common/util/parse-animation-duration";
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));
};
import { withViewTransition } from "../common/util/view-transition";
export const removeLaunchScreen = () => {
const launchScreenElement = document.getElementById("ha-launch-screen");
@@ -28,14 +9,22 @@ export const removeLaunchScreen = () => {
return;
}
if (document.startViewTransition) {
document.startViewTransition(() => {
removeElement(launchScreenElement, false);
});
} else {
// Fallback: Direct removal without transition
removeElement(launchScreenElement, true);
}
withViewTransition((viewTransitionAvailable: boolean) => {
if (!viewTransitionAvailable) {
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 renderLaunchScreenInfoBox = (content: TemplateResult) => {

View File

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