diff --git a/src/data/automation.ts b/src/data/automation.ts index 0e3b2fb6df..9778a12dda 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -167,7 +167,7 @@ export interface TagTrigger extends BaseTrigger { export interface TimeTrigger extends BaseTrigger { trigger: "time"; - at: string; + at: string | { entity_id: string; offset?: string }; } export interface TemplateTrigger extends BaseTrigger { diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index c6bbbbeb77..1e4fc0722e 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -8,6 +8,7 @@ import { import secondsToDuration from "../common/datetime/seconds_to_duration"; import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display"; import { computeStateName } from "../common/entity/compute_state_name"; +import { isValidEntityId } from "../common/entity/valid_entity_id"; import type { HomeAssistant } from "../types"; import { Condition, ForDict, Trigger } from "./automation"; import { @@ -371,13 +372,22 @@ const tryDescribeTrigger = ( // Time Trigger if (trigger.trigger === "time" && trigger.at) { - const result = ensureArray(trigger.at).map((at) => - typeof at !== "string" - ? at - : at.includes(".") - ? `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}` - : localizeTimeString(at, hass.locale, hass.config) - ); + const result = ensureArray(trigger.at).map((at) => { + if (typeof at === "string") { + if (isValidEntityId(at)) { + return `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`; + } + return localizeTimeString(at, hass.locale, hass.config); + } + const entityStr = `entity ${hass.states[at.entity_id] ? computeStateName(hass.states[at.entity_id]) : at.entity_id}`; + const offsetStr = at.offset + ? " " + + hass.localize(`${triggerTranslationBaseKey}.time.offset_by`, { + offset: describeDuration(hass.locale, at.offset), + }) + : ""; + return `${entityStr}${offsetStr}`; + }); return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, { time: formatListWithOrs(hass.locale, result), diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts index 9009cb3217..f2cfdc2f32 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts @@ -9,6 +9,9 @@ import type { TimeTrigger } from "../../../../../data/automation"; import type { HomeAssistant } from "../../../../../types"; import type { TriggerElement } from "../ha-automation-trigger-row"; +const MODE_TIME = "time"; +const MODE_ENTITY = "entity"; + @customElement("ha-automation-trigger-time") export class HaTimeTrigger extends LitElement implements TriggerElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -17,48 +20,60 @@ export class HaTimeTrigger extends LitElement implements TriggerElement { @property({ type: Boolean }) public disabled = false; - @state() private _inputMode?: boolean; + @state() private _inputMode: + | undefined + | typeof MODE_TIME + | typeof MODE_ENTITY; public static get defaultConfig(): TimeTrigger { return { trigger: "time", at: "" }; } private _schema = memoizeOne( - (localize: LocalizeFunc, inputMode?: boolean) => { - const atSelector = inputMode - ? { - entity: { - filter: [ - { domain: "input_datetime" }, - { domain: "sensor", device_class: "timestamp" }, - ], - }, - } - : { time: {} }; - - return [ + ( + localize: LocalizeFunc, + inputMode: typeof MODE_TIME | typeof MODE_ENTITY, + showOffset: boolean + ) => + [ { name: "mode", type: "select", required: true, options: [ [ - "value", + MODE_TIME, localize( "ui.panel.config.automation.editor.triggers.type.time.type_value" ), ], [ - "input", + MODE_ENTITY, localize( "ui.panel.config.automation.editor.triggers.type.time.type_input" ), ], ], }, - { name: "at", selector: atSelector }, - ] as const; - } + ...(inputMode === MODE_TIME + ? ([{ name: "time", selector: { time: {} } }] as const) + : ([ + { + name: "entity", + selector: { + entity: { + filter: [ + { domain: "input_datetime" }, + { domain: "sensor", device_class: "timestamp" }, + ], + }, + }, + }, + ] as const)), + ...(showOffset + ? ([{ name: "offset", selector: { text: {} } }] as const) + : ([] as const)), + ] as const ); public willUpdate(changedProperties: PropertyValues) { @@ -75,23 +90,46 @@ export class HaTimeTrigger extends LitElement implements TriggerElement { } } + private _data = memoizeOne( + ( + inputMode: undefined | typeof MODE_ENTITY | typeof MODE_TIME, + at: + | string + | { entity_id: string | undefined; offset?: string | undefined } + ): { + mode: typeof MODE_TIME | typeof MODE_ENTITY; + entity: string | undefined; + time: string | undefined; + offset: string | undefined; + } => { + const entity = + typeof at === "object" + ? at.entity_id + : at?.startsWith("input_datetime.") || at?.startsWith("sensor.") + ? at + : undefined; + const time = entity ? undefined : (at as string | undefined); + const offset = typeof at === "object" ? at.offset : undefined; + const mode = inputMode ?? (entity ? MODE_ENTITY : MODE_TIME); + return { + mode, + entity, + time, + offset, + }; + } + ); + protected render() { const at = this.trigger.at; if (Array.isArray(at)) { return nothing; } - - const inputMode = - this._inputMode ?? - (at?.startsWith("input_datetime.") || at?.startsWith("sensor.")); - - const schema = this._schema(this.hass.localize, inputMode); - - const data = { - mode: inputMode ? "input" : "value", - ...this.trigger, - }; + const data = this._data(this._inputMode, at); + const showOffset = + data.mode === MODE_ENTITY && data.entity?.startsWith("sensor."); + const schema = this._schema(this.hass.localize, data.mode, !!showOffset); return html` - newValue[key] === undefined || newValue[key] === "" - ? delete newValue[key] - : {} - ); - - fireEvent(this, "value-changed", { value: newValue }); + const newValue = { ...ev.detail.value }; + this._inputMode = newValue.mode; + if (newValue.mode === MODE_TIME) { + delete newValue.entity; + delete newValue.offset; + } else { + delete newValue.time; + if (!newValue.entity?.startsWith("sensor.")) { + delete newValue.offset; + } + } + fireEvent(this, "value-changed", { + value: { + ...this.trigger, + at: newValue.offset + ? { + entity_id: newValue.entity, + offset: newValue.offset, + } + : newValue.entity || newValue.time, + }, + }); } private _computeLabelCallback = ( schema: SchemaUnion> - ): string => - this.hass.localize( + ): string => { + switch (schema.name) { + case "time": + return this.hass.localize( + `ui.panel.config.automation.editor.triggers.type.time.at` + ); + } + return this.hass.localize( `ui.panel.config.automation.editor.triggers.type.time.${schema.name}` ); + }; } declare global { diff --git a/src/translations/en.json b/src/translations/en.json index 3b923c442b..8fc52ddd5c 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3054,6 +3054,9 @@ "type_input": "Value of a date/time helper or timestamp-class sensor", "label": "Time", "at": "At time", + "offset": "[%key:ui::panel::config::automation::editor::triggers::type::sun::offset%]", + "entity": "Entity with timestamp", + "offset_by": "offset by {offset}", "mode": "Mode", "description": { "picker": "At a specific time, or on a specific date.",