From 5dad18c85fa8f387682979a6130fc36568083fed Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 12 Aug 2021 22:52:26 +0200 Subject: [PATCH] Make time inputs the same through the UI (#9766) --- src/common/datetime/create_duration_data.ts | 31 +++ src/components/ha-duration-input.ts | 140 +++++++++++++ .../ha-form-positive_time_period_dict.ts | 6 +- src/components/ha-form/ha-form.ts | 4 +- .../ha-selector/ha-selector-time.ts | 42 +--- src/components/ha-time-input.ts | 160 +++++---------- src/data/automation.ts | 8 +- .../controls/more-info-input_datetime.js | 191 ------------------ .../controls/more-info-input_datetime.ts | 103 ++++++++++ .../types/ha-automation-action-delay.ts | 39 +--- .../types/ha-automation-condition-state.ts | 28 +-- .../types/ha-automation-condition-time.ts | 16 +- .../trigger/ha-automation-trigger-row.ts | 27 ++- .../ha-automation-trigger-numeric_state.ts | 47 +++-- .../types/ha-automation-trigger-state.ts | 46 +++-- .../types/ha-automation-trigger-time.ts | 54 +++-- .../hui-input-datetime-entity-row.ts | 77 ++++--- 17 files changed, 528 insertions(+), 491 deletions(-) create mode 100644 src/common/datetime/create_duration_data.ts create mode 100644 src/components/ha-duration-input.ts delete mode 100644 src/dialogs/more-info/controls/more-info-input_datetime.js create mode 100644 src/dialogs/more-info/controls/more-info-input_datetime.ts diff --git a/src/common/datetime/create_duration_data.ts b/src/common/datetime/create_duration_data.ts new file mode 100644 index 0000000000..92b3d01021 --- /dev/null +++ b/src/common/datetime/create_duration_data.ts @@ -0,0 +1,31 @@ +import { HaDurationData } from "../../components/ha-duration-input"; +import { ForDict } from "../../data/automation"; + +export const createDurationData = ( + duration: string | number | ForDict | undefined +): HaDurationData => { + if (duration === undefined) { + return {}; + } + if (typeof duration !== "object") { + if (typeof duration === "string" || isNaN(duration)) { + const parts = duration?.toString().split(":") || []; + return { + hours: Number(parts[0]) || 0, + minutes: Number(parts[1]) || 0, + seconds: Number(parts[2]) || 0, + milliseconds: Number(parts[3]) || 0, + }; + } + return { seconds: duration }; + } + const { days, minutes, seconds, milliseconds } = duration; + let hours = duration.hours || 0; + hours = (hours || 0) + (days || 0) * 24; + return { + hours, + minutes, + seconds, + milliseconds, + }; +}; diff --git a/src/components/ha-duration-input.ts b/src/components/ha-duration-input.ts new file mode 100644 index 0000000000..32651cb2c2 --- /dev/null +++ b/src/components/ha-duration-input.ts @@ -0,0 +1,140 @@ +import { html, LitElement, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; +import "./paper-time-input"; + +export interface HaDurationData { + hours?: number; + minutes?: number; + seconds?: number; + milliseconds?: number; +} + +@customElement("ha-duration-input") +class HaDurationInput extends LitElement { + @property({ attribute: false }) public data!: HaDurationData; + + @property() public label?: string; + + @property() public suffix?: string; + + @property({ type: Boolean }) public required?: boolean; + + @property({ type: Boolean }) public enableMillisecond?: boolean; + + @query("paper-time-input", true) private _input?: HTMLElement; + + public focus() { + if (this._input) { + this._input.focus(); + } + } + + protected render(): TemplateResult { + return html` + + `; + } + + private get _hours() { + return this.data && this.data.hours ? Number(this.data.hours) : 0; + } + + private get _minutes() { + return this.data && this.data.minutes ? Number(this.data.minutes) : 0; + } + + private get _seconds() { + return this.data && this.data.seconds ? Number(this.data.seconds) : 0; + } + + private get _milliseconds() { + return this.data && this.data.milliseconds + ? Number(this.data.milliseconds) + : 0; + } + + private _parseDuration(value) { + return value.toString().padStart(2, "0"); + } + + private _parseDurationMillisec(value) { + return value.toString().padStart(3, "0"); + } + + private _hourChanged(ev) { + this._durationChanged(ev, "hours"); + } + + private _minChanged(ev) { + this._durationChanged(ev, "minutes"); + } + + private _secChanged(ev) { + this._durationChanged(ev, "seconds"); + } + + private _millisecChanged(ev) { + this._durationChanged(ev, "milliseconds"); + } + + private _durationChanged(ev, unit) { + let value = Number(ev.detail.value); + + if (value === this[`_${unit}`]) { + return; + } + + let hours = this._hours; + let minutes = this._minutes; + + if (unit === "seconds" && value > 59) { + minutes += Math.floor(value / 60); + value %= 60; + } + + if (unit === "minutes" && value > 59) { + hours += Math.floor(value / 60); + value %= 60; + } + + fireEvent(this, "value-changed", { + value: { + hours, + minutes, + seconds: this._seconds, + milliseconds: this._milliseconds, + ...{ [unit]: value }, + }, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-duration-input": HaDurationInput; + } +} diff --git a/src/components/ha-form/ha-form-positive_time_period_dict.ts b/src/components/ha-form/ha-form-positive_time_period_dict.ts index 04ec575981..7806e5c685 100644 --- a/src/components/ha-form/ha-form-positive_time_period_dict.ts +++ b/src/components/ha-form/ha-form-positive_time_period_dict.ts @@ -1,6 +1,6 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; -import "../ha-time-input"; +import "../ha-duration-input"; import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./ha-form"; @customElement("ha-form-positive_time_period_dict") @@ -23,11 +23,11 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement { protected render(): TemplateResult { return html` - + > `; } } diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index 1a4c02e319..49b19a8328 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -2,7 +2,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../common/dom/fire_event"; -import { HaTimeData } from "../ha-time-input"; +import { HaDurationData } from "../ha-duration-input"; import "./ha-form-boolean"; import "./ha-form-constant"; import "./ha-form-float"; @@ -88,7 +88,7 @@ export type HaFormFloatData = number; export type HaFormBooleanData = boolean; export type HaFormSelectData = string; export type HaFormMultiSelectData = string[]; -export type HaFormTimeData = HaTimeData; +export type HaFormTimeData = HaDurationData; export interface HaFormElement extends LitElement { schema: HaFormSchema | HaFormSchema[]; diff --git a/src/components/ha-selector/ha-selector-time.ts b/src/components/ha-selector/ha-selector-time.ts index 9ac9ea8e51..f1a116d371 100644 --- a/src/components/ha-selector/ha-selector-time.ts +++ b/src/components/ha-selector/ha-selector-time.ts @@ -1,10 +1,8 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { useAmPm } from "../../common/datetime/use_am_pm"; -import { fireEvent } from "../../common/dom/fire_event"; import { TimeSelector } from "../../data/selector"; import { HomeAssistant } from "../../types"; -import "../paper-time-input"; +import "../ha-time-input"; @customElement("ha-selector-time") export class HaTimeSelector extends LitElement { @@ -19,46 +17,16 @@ export class HaTimeSelector extends LitElement { @property({ type: Boolean }) public disabled = false; protected render() { - const useAMPM = useAmPm(this.hass.locale); - - const parts = this.value?.split(":") || []; - const hours = parts[0]; - return html` - 12 ? Number(hours) - 12 : hours)} - .min=${parts[1]} - .sec=${parts[2]} - .format=${useAMPM ? 12 : 24} - .amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")} + + > `; } - - private _timeChanged(ev) { - let value = ev.target.value; - const useAMPM = useAmPm(this.hass.locale); - let hours = Number(ev.target.hour || 0); - if (value && useAMPM) { - if (ev.target.amPm === "PM") { - hours += 12; - } - value = `${hours}:${ev.target.min || "00"}:${ev.target.sec || "00"}`; - } - if (value === this.value) { - return; - } - fireEvent(this, "value-changed", { - value, - }); - } } declare global { diff --git a/src/components/ha-time-input.ts b/src/components/ha-time-input.ts index 382ea4d569..c4ca542801 100644 --- a/src/components/ha-time-input.ts +++ b/src/components/ha-time-input.ts @@ -1,134 +1,78 @@ -import { html, LitElement, TemplateResult } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { useAmPm } from "../common/datetime/use_am_pm"; import { fireEvent } from "../common/dom/fire_event"; import "./paper-time-input"; - -export interface HaTimeData { - hours?: number; - minutes?: number; - seconds?: number; - milliseconds?: number; -} +import { FrontendLocaleData } from "../data/translation"; @customElement("ha-time-input") -class HaTimeInput extends LitElement { - @property() public data!: HaTimeData; +export class HaTimeInput extends LitElement { + @property() public locale!: FrontendLocaleData; + + @property() public value?: string; @property() public label?: string; - @property() public suffix?: string; + @property({ type: Boolean }) public disabled = false; - @property({ type: Boolean }) public required?: boolean; + @property({ type: Boolean, attribute: "hide-label" }) public hideLabel = + false; - @property({ type: Boolean }) public enableMillisecond?: boolean; + @property({ type: Boolean, attribute: "enable-second" }) + public enableSecond = false; - @query("paper-time-input", true) private _input?: HTMLElement; + protected render() { + const useAMPM = useAmPm(this.locale); - public focus() { - if (this._input) { - this._input.focus(); + const parts = this.value?.split(":") || []; + let hours = parts[0]; + const numberHours = Number(parts[0]); + if (numberHours && useAMPM && numberHours > 12) { + hours = String(numberHours - 12).padStart(2, "0"); + } + if (useAMPM && numberHours === 0) { + hours = "12"; } - } - protected render(): TemplateResult { return html` = 12 ? "PM" : "AM")} + .disabled=${this.disabled} + @change=${this._timeChanged} + @am-pm-changed=${this._timeChanged} + .hideLabel=${this.hideLabel} + .enableSecond=${this.enableSecond} > `; } - private get _hours() { - return this.data && this.data.hours ? Number(this.data.hours) : 0; - } - - private get _minutes() { - return this.data && this.data.minutes ? Number(this.data.minutes) : 0; - } - - private get _seconds() { - return this.data && this.data.seconds ? Number(this.data.seconds) : 0; - } - - private get _milliseconds() { - return this.data && this.data.milliseconds - ? Number(this.data.milliseconds) - : 0; - } - - private _parseDuration(value) { - return value.toString().padStart(2, "0"); - } - - private _parseDurationMillisec(value) { - return value.toString().padStart(3, "0"); - } - - private _hourChanged(ev) { - this._durationChanged(ev, "hours"); - } - - private _minChanged(ev) { - this._durationChanged(ev, "minutes"); - } - - private _secChanged(ev) { - this._durationChanged(ev, "seconds"); - } - - private _millisecChanged(ev) { - this._durationChanged(ev, "milliseconds"); - } - - private _durationChanged(ev, unit) { - let value = Number(ev.detail.value); - - if (value === this[`_${unit}`]) { + private _timeChanged(ev) { + let value = ev.target.value; + const useAMPM = useAmPm(this.locale); + let hours = Number(ev.target.hour || 0); + if (value && useAMPM) { + if (ev.target.amPm === "PM" && hours < 12) { + hours += 12; + } + if (ev.target.amPm === "AM" && hours === 12) { + hours = 0; + } + value = `${hours.toString().padStart(2, "0")}:${ev.target.min || "00"}:${ + ev.target.sec || "00" + }`; + } + if (value === this.value) { return; } - - let hours = this._hours; - let minutes = this._minutes; - - if (unit === "seconds" && value > 59) { - minutes += Math.floor(value / 60); - value %= 60; - } - - if (unit === "minutes" && value > 59) { - hours += Math.floor(value / 60); - value %= 60; - } - + this.value = value; + fireEvent(this, "change"); fireEvent(this, "value-changed", { - value: { - hours, - minutes, - seconds: this._seconds, - milliseconds: this._milliseconds, - ...{ [unit]: value }, - }, + value, }); } } diff --git a/src/data/automation.ts b/src/data/automation.ts index da6a12ddba..b6ea4c90c6 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -46,9 +46,11 @@ export interface BlueprintAutomationConfig extends ManualAutomationConfig { } export interface ForDict { - hours?: number | string; - minutes?: number | string; - seconds?: number | string; + days?: number; + hours?: number; + minutes?: number; + seconds?: number; + milliseconds?: number; } export interface ContextConstraint { diff --git a/src/dialogs/more-info/controls/more-info-input_datetime.js b/src/dialogs/more-info/controls/more-info-input_datetime.js deleted file mode 100644 index cff1b5233b..0000000000 --- a/src/dialogs/more-info/controls/more-info-input_datetime.js +++ /dev/null @@ -1,191 +0,0 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import "@polymer/paper-input/paper-input"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { attributeClassNames } from "../../../common/entity/attribute_class_names"; -import "../../../components/ha-date-input"; -import "../../../components/ha-relative-time"; -import "../../../components/paper-time-input"; - -class DatetimeInput extends PolymerElement { - static get template() { - return html` -
- - -
- `; - } - - constructor() { - super(); - this.is_ready = false; - } - - static get properties() { - return { - hass: { - type: Object, - }, - - stateObj: { - type: Object, - observer: "stateObjChanged", - }, - - selectedDate: { - type: String, - observer: "dateTimeChanged", - }, - - selectedHour: { - type: Number, - observer: "dateTimeChanged", - }, - - selectedMinute: { - type: Number, - observer: "dateTimeChanged", - }, - }; - } - - ready() { - super.ready(); - this.is_ready = true; - } - - /* Convert the date in the stateObj into a string useable by vaadin-date-picker */ - getDateString(stateObj) { - if (stateObj.state === "unknown") { - return ""; - } - let monthFiller; - if (stateObj.attributes.month < 10) { - monthFiller = "0"; - } else { - monthFiller = ""; - } - - let dayFiller; - if (stateObj.attributes.day < 10) { - dayFiller = "0"; - } else { - dayFiller = ""; - } - - return ( - stateObj.attributes.year + - "-" + - monthFiller + - stateObj.attributes.month + - "-" + - dayFiller + - stateObj.attributes.day - ); - } - - /* Should fire when any value was changed *by the user*, not b/c of setting - * initial values. */ - dateTimeChanged() { - // Check if the change is really coming from the user - if (!this.is_ready) { - return; - } - - let changed = false; - let minuteFiller; - - const serviceData = { - entity_id: this.stateObj.entity_id, - }; - - if (this.stateObj.attributes.has_time) { - changed = - changed || - parseInt(this.selectedMinute) !== this.stateObj.attributes.minute; - changed = - changed || - parseInt(this.selectedHour) !== this.stateObj.attributes.hour; - if (this.selectedMinute < 10) { - minuteFiller = "0"; - } else { - minuteFiller = ""; - } - const timeStr = - this.selectedHour + ":" + minuteFiller + this.selectedMinute; - serviceData.time = timeStr; - } - - if (this.stateObj.attributes.has_date) { - if (this.selectedDate.length === 0) { - return; // Date was not set - } - - const dateValInput = new Date(this.selectedDate); - const dateValState = new Date( - this.stateObj.attributes.year, - this.stateObj.attributes.month - 1, - this.stateObj.attributes.day - ); - - changed = changed || dateValState !== dateValInput; - - serviceData.date = this.selectedDate; - } - - if (changed) { - this.hass.callService("input_datetime", "set_datetime", serviceData); - } - } - - stateObjChanged(newVal) { - // Set to non-ready s.t. dateTimeChanged does not fire - this.is_ready = false; - - if (newVal.attributes.has_time) { - this.selectedHour = newVal.attributes.hour; - this.selectedMinute = newVal.attributes.minute; - } - - if (newVal.attributes.has_date) { - this.selectedDate = this.getDateString(newVal); - } - - this.is_ready = true; - } - - doesHaveDate(stateObj) { - return stateObj.attributes.has_date; - } - - doesHaveTime(stateObj) { - return stateObj.attributes.has_time; - } - - computeClassNames(stateObj) { - return ( - "more-info-input_datetime " + - attributeClassNames(stateObj, ["has_time", "has_date"]) - ); - } -} - -customElements.define("more-info-input_datetime", DatetimeInput); diff --git a/src/dialogs/more-info/controls/more-info-input_datetime.ts b/src/dialogs/more-info/controls/more-info-input_datetime.ts new file mode 100644 index 0000000000..af1e9aa6a1 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-input_datetime.ts @@ -0,0 +1,103 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import "../../../components/ha-date-input"; +import "../../../components/ha-time-input"; +import { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; +import { setInputDateTimeValue } from "../../../data/input_datetime"; +import type { HomeAssistant } from "../../../types"; + +@customElement("more-info-input_datetime") +class MoreInfoInputDatetime extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj?: HassEntity; + + protected render(): TemplateResult { + if (!this.stateObj) { + return html``; + } + + return html` + ${ + this.stateObj.attributes.has_date + ? html` + + + ` + : `` + } + ${ + this.stateObj.attributes.has_time + ? html` + + ` + : `` + } + + `; + } + + private _stopEventPropagation(ev: Event): void { + ev.stopPropagation(); + } + + private _timeChanged(ev): void { + setInputDateTimeValue( + this.hass!, + this.stateObj!.entity_id, + ev.detail.value, + this.stateObj!.attributes.has_date + ? this.stateObj!.state.split(" ")[0] + : undefined + ); + ev.target.blur(); + } + + private _dateChanged(ev): void { + setInputDateTimeValue( + this.hass!, + this.stateObj!.entity_id, + this.stateObj!.attributes.has_time + ? this.stateObj!.state.split(" ")[1] + : undefined, + ev.detail.value + ); + + ev.target.blur(); + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: flex; + align-items: center; + justify-content: flex-end; + } + ha-date-input + ha-time-input { + margin-left: 4px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-input_datetime": MoreInfoInputDatetime; + } +} diff --git a/src/panels/config/automation/action/types/ha-automation-action-delay.ts b/src/panels/config/automation/action/types/ha-automation-action-delay.ts index ab9a3ed8ab..e03a533be6 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-delay.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-delay.ts @@ -1,14 +1,13 @@ -import "@polymer/paper-input/paper-input"; import { html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { hasTemplate } from "../../../../../common/string/has-template"; -import "../../../../../components/entity/ha-entity-picker"; -import { HaFormTimeData } from "../../../../../components/ha-form/ha-form"; -import "../../../../../components/ha-service-picker"; +import type { HaDurationData } from "../../../../../components/ha-duration-input"; +import "../../../../../components/ha-duration-input"; import { DelayAction } from "../../../../../data/script"; import { HomeAssistant } from "../../../../../types"; import { ActionElement } from "../ha-automation-action-row"; +import { createDurationData } from "../../../../../common/datetime/create_duration_data"; @customElement("ha-automation-action-delay") export class HaDelayAction extends LitElement implements ActionElement { @@ -16,13 +15,13 @@ export class HaDelayAction extends LitElement implements ActionElement { @property() public action!: DelayAction; - @property() public _timeData!: HaFormTimeData; + @property() public _timeData!: HaDurationData; public static get defaultConfig() { return { delay: "" }; } - protected updated(changedProperties: PropertyValues) { + public willUpdate(changedProperties: PropertyValues) { if (!changedProperties.has("action")) { return; } @@ -36,37 +35,15 @@ export class HaDelayAction extends LitElement implements ActionElement { return; } - if (typeof this.action.delay !== "object") { - if (typeof this.action.delay === "string" || isNaN(this.action.delay)) { - const parts = this.action.delay?.toString().split(":") || []; - this._timeData = { - hours: Number(parts[0]) || 0, - minutes: Number(parts[1]) || 0, - seconds: Number(parts[2]) || 0, - milliseconds: Number(parts[3]) || 0, - }; - } else { - this._timeData = { seconds: this.action.delay }; - } - return; - } - const { days, minutes, seconds, milliseconds } = this.action.delay; - let { hours } = this.action.delay || 0; - hours = (hours || 0) + (days || 0) * 24; - this._timeData = { - hours: hours, - minutes: minutes, - seconds: seconds, - milliseconds: milliseconds, - }; + this._timeData = createDurationData(this.action.delay); } protected render() { - return html``; + >`; } private _valueChanged(ev: CustomEvent) { diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-state.ts b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts index 1b841f0af9..22103067f8 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-state.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-state.ts @@ -1,14 +1,16 @@ import "@polymer/paper-input/paper-input"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import { createDurationData } from "../../../../../common/datetime/create_duration_data"; import "../../../../../components/entity/ha-entity-attribute-picker"; import "../../../../../components/entity/ha-entity-picker"; -import { ForDict, StateCondition } from "../../../../../data/automation"; +import { StateCondition } from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; import { ConditionElement, handleChangeEvent, } from "../ha-automation-condition-row"; +import "../../../../../components/ha-duration-input"; @customElement("ha-automation-condition-state") export class HaStateCondition extends LitElement implements ConditionElement { @@ -22,23 +24,7 @@ export class HaStateCondition extends LitElement implements ConditionElement { protected render() { const { entity_id, attribute, state } = this.condition; - let forTime = this.condition.for; - - if ( - forTime && - ((forTime as ForDict).hours || - (forTime as ForDict).minutes || - (forTime as ForDict).seconds) - ) { - // If the trigger was defined using the yaml dict syntax, convert it to - // the equivalent string format - let { hours = 0, minutes = 0, seconds = 0 } = forTime as ForDict; - hours = hours.toString().padStart(2, "0"); - minutes = minutes.toString().padStart(2, "0"); - seconds = seconds.toString().padStart(2, "0"); - - forTime = `${hours}:${minutes}:${seconds}`; - } + const forTime = createDurationData(this.condition.for); return html` - + > `; } diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-time.ts b/src/panels/config/automation/condition/types/ha-automation-condition-time.ts index e606e31eac..f227847a3f 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-time.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-time.ts @@ -1,5 +1,4 @@ import { Radio } from "@material/mwc-radio"; -import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; @@ -13,6 +12,7 @@ import { ConditionElement, handleChangeEvent, } from "../ha-automation-condition-row"; +import "../../../../../components/ha-time-input"; const includeDomains = ["input_datetime"]; @@ -89,14 +89,15 @@ export class HaTimeCondition extends LitElement implements ConditionElement { .hass=${this.hass} allow-custom-entity >` - : html``} + >`} ` - : html``} + >`} ${Object.keys(DAYS).map( (day) => html` + ${this._warnings + ? html`
+ ${this.hass.localize("ui.errors.config.editor_not_supported")}: +
+ ${this._warnings.length && this._warnings[0] !== undefined + ? html`
    + ${this._warnings.map( + (warning) => html`
  • ${warning}
  • ` + )} +
` + : ""} + ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} +
` + : ""} ${yamlMode ? html` ${selected === -1 @@ -170,7 +187,7 @@ export default class HaAutomationTriggerRow extends LitElement { @value-changed=${this._idChanged} > -
+
${dynamicElement( `ha-automation-trigger-${this.trigger.platform}`, { hass: this.hass, trigger: this.trigger } @@ -182,6 +199,13 @@ export default class HaAutomationTriggerRow extends LitElement { `; } + private _handleUiModeNotAvailable(ev: CustomEvent) { + this._warnings = handleStructError(this.hass, ev.detail).warnings; + if (!this._yamlMode) { + this._yamlMode = true; + } + } + private _handleAction(ev: CustomEvent) { switch (ev.detail.index) { case 0: @@ -258,6 +282,7 @@ export default class HaAutomationTriggerRow extends LitElement { } private _switchYamlMode() { + this._warnings = undefined; this._yamlMode = !this._yamlMode; } diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts index cf7cf64115..27c2c6d708 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts @@ -1,11 +1,15 @@ import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-textarea"; -import { html, LitElement } from "lit"; +import { html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; +import { createDurationData } from "../../../../../common/datetime/create_duration_data"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { hasTemplate } from "../../../../../common/string/has-template"; import "../../../../../components/entity/ha-entity-picker"; -import { ForDict, NumericStateTrigger } from "../../../../../data/automation"; +import { NumericStateTrigger } from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; import { handleChangeEvent } from "../ha-automation-trigger-row"; +import "../../../../../components/ha-duration-input"; @customElement("ha-automation-trigger-numeric_state") export default class HaNumericStateTrigger extends LitElement { @@ -13,6 +17,20 @@ export default class HaNumericStateTrigger extends LitElement { @property() public trigger!: NumericStateTrigger; + public willUpdate(changedProperties: PropertyValues) { + if (!changedProperties.has("trigger")) { + return; + } + // Check for templates in trigger. If found, revert to YAML mode. + if (this.trigger && hasTemplate(this.trigger)) { + fireEvent( + this, + "ui-mode-not-available", + Error(this.hass.localize("ui.errors.config.no_template_editor_support")) + ); + } + } + public static get defaultConfig() { return { entity_id: "", @@ -21,23 +39,8 @@ export default class HaNumericStateTrigger extends LitElement { public render() { const { value_template, entity_id, attribute, below, above } = this.trigger; - let trgFor = this.trigger.for; + const trgFor = createDurationData(this.trigger.for); - if ( - trgFor && - ((trgFor as ForDict).hours || - (trgFor as ForDict).minutes || - (trgFor as ForDict).seconds) - ) { - // If the trigger was defined using the yaml dict syntax, convert it to - // the equivalent string format - let { hours = 0, minutes = 0, seconds = 0 } = trgFor as ForDict; - hours = hours.toString(); - minutes = minutes.toString().padStart(2, "0"); - seconds = seconds.toString().padStart(2, "0"); - - trgFor = `${hours}:${minutes}:${seconds}`; - } return html` - + > `; } diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts index 1ad3b18555..6be2d30fab 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-state.ts @@ -1,10 +1,14 @@ import "@polymer/paper-input/paper-input"; -import { html, LitElement } from "lit"; +import { html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; +import { createDurationData } from "../../../../../common/datetime/create_duration_data"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { hasTemplate } from "../../../../../common/string/has-template"; import "../../../../../components/entity/ha-entity-attribute-picker"; import "../../../../../components/entity/ha-entity-picker"; -import { ForDict, StateTrigger } from "../../../../../data/automation"; +import { StateTrigger } from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; +import "../../../../../components/ha-duration-input"; import { handleChangeEvent, TriggerElement, @@ -20,25 +24,23 @@ export class HaStateTrigger extends LitElement implements TriggerElement { return { entity_id: "" }; } + public willUpdate(changedProperties: PropertyValues) { + if (!changedProperties.has("trigger")) { + return; + } + // Check for templates in trigger. If found, revert to YAML mode. + if (this.trigger && hasTemplate(this.trigger)) { + fireEvent( + this, + "ui-mode-not-available", + Error(this.hass.localize("ui.errors.config.no_template_editor_support")) + ); + } + } + protected render() { const { entity_id, attribute, to, from } = this.trigger; - let trgFor = this.trigger.for; - - if ( - trgFor && - ((trgFor as ForDict).hours || - (trgFor as ForDict).minutes || - (trgFor as ForDict).seconds) - ) { - // If the trigger was defined using the yaml dict syntax, convert it to - // the equivalent string format - let { hours = 0, minutes = 0, seconds = 0 } = trgFor as ForDict; - hours = hours.toString(); - minutes = minutes.toString().padStart(2, "0"); - seconds = seconds.toString().padStart(2, "0"); - - trgFor = `${hours}:${minutes}:${seconds}`; - } + const trgFor = createDurationData(this.trigger.for); return html` - + > `; } 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 ddb0404907..a72c5a9749 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 @@ -1,5 +1,4 @@ -import "@polymer/paper-input/paper-input"; -import { html, LitElement } from "lit"; +import { html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../../../../components/entity/ha-entity-picker"; import "../../../../../components/ha-formfield"; @@ -10,9 +9,10 @@ import { handleChangeEvent, TriggerElement, } from "../ha-automation-trigger-row"; +import "../../../../../components/ha-time-input"; +import { fireEvent } from "../../../../../common/dom/fire_event"; const includeDomains = ["input_datetime"]; - @customElement("ha-automation-trigger-time") export class HaTimeTrigger extends LitElement implements TriggerElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -25,11 +25,32 @@ export class HaTimeTrigger extends LitElement implements TriggerElement { return { at: "" }; } + public willUpdate(changedProperties: PropertyValues) { + if (!changedProperties.has("trigger")) { + return; + } + // We dont support multiple times atm. + if (this.trigger && Array.isArray(this.trigger.at)) { + fireEvent( + this, + "ui-mode-not-available", + Error(this.hass.localize("ui.errors.config.editor_not_supported")) + ); + } + } + protected render() { - const { at } = this.trigger; - const inputMode = this._inputMode ?? at?.startsWith("input_datetime."); - return html` - + ${inputMode ? html`` - : html``} - `; + >`} `; } private _handleModeChanged(ev: Event) { diff --git a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts index 0567009b12..c3441ac769 100644 --- a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts @@ -1,9 +1,13 @@ -import { html, LitElement, PropertyValues, TemplateResult } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; import "../../../components/ha-date-input"; -import type { HaDateInput } from "../../../components/ha-date-input"; -import "../../../components/paper-time-input"; -import type { PaperTimeInput } from "../../../components/paper-time-input"; import { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; import { setInputDateTimeValue } from "../../../data/input_datetime"; import type { HomeAssistant } from "../../../types"; @@ -11,6 +15,7 @@ import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import type { EntityConfig, LovelaceRow } from "./types"; +import "../../../components/ha-time-input"; @customElement("hui-input-datetime-entity-row") class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { @@ -18,10 +23,6 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { @state() private _config?: EntityConfig; - @query("paper-time-input") private _timeInputEl?: PaperTimeInput; - - @query("ha-date-input") private _dateInputEl?: HaDateInput; - public setConfig(config: EntityConfig): void { if (!config) { throw new Error("Invalid configuration"); @@ -55,27 +56,25 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { - ${stateObj.attributes.has_time ? "," : ""} ` : ``} ${stateObj.attributes.has_time ? html` - + @value-changed=${this._timeChanged} + @click=${this._stopEventPropagation} + > ` : ``} @@ -86,19 +85,37 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { ev.stopPropagation(); } - private _selectedValueChanged(ev): void { + private _timeChanged(ev): void { + const stateObj = this.hass!.states[this._config!.entity]; + setInputDateTimeValue( + this.hass!, + stateObj.entity_id, + ev.detail.value, + stateObj.attributes.has_date ? stateObj.state.split(" ")[0] : undefined + ); + ev.target.blur(); + } + + private _dateChanged(ev): void { const stateObj = this.hass!.states[this._config!.entity]; - const time = this._timeInputEl - ? this._timeInputEl.value?.trim() - : undefined; - - const date = this._dateInputEl ? this._dateInputEl.value : undefined; - - setInputDateTimeValue(this.hass!, stateObj.entity_id, time, date); + setInputDateTimeValue( + this.hass!, + stateObj.entity_id, + stateObj.attributes.has_time ? stateObj.state.split(" ")[1] : undefined, + ev.detail.value + ); ev.target.blur(); } + + static get styles(): CSSResultGroup { + return css` + ha-date-input + ha-time-input { + margin-left: 4px; + } + `; + } } declare global {