Skip to content

Commit

Permalink
Add sensor offset to time trigger UI (#21957)
Browse files Browse the repository at this point in the history
* Add sensor offset to time trigger UI

* refactor long expression

* memoize data

* fix for trigger platform migration
  • Loading branch information
karwosts authored Oct 11, 2024
1 parent 82b50a1 commit 79c71cb
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 52 deletions.
2 changes: 1 addition & 1 deletion src/data/automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
24 changes: 17 additions & 7 deletions src/data/automation_i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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`
<ha-form
Expand All @@ -107,26 +145,43 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {

private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newValue = ev.detail.value;

this._inputMode = newValue.mode === "input";
delete newValue.mode;

Object.keys(newValue).forEach((key) =>
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<ReturnType<typeof this._schema>>
): 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 {
Expand Down
3 changes: 3 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down

0 comments on commit 79c71cb

Please sign in to comment.