Make time inputs the same through the UI (#9766)

This commit is contained in:
Bram Kragten 2021-08-12 22:52:26 +02:00 committed by GitHub
parent 19e4c0657a
commit 5dad18c85f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 528 additions and 491 deletions

View File

@ -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,
};
};

View File

@ -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`
<paper-time-input
.label=${this.label}
.required=${this.required}
.autoValidate=${this.required}
error-message="Required"
enable-second
.enableMillisecond=${this.enableMillisecond}
format="24"
.hour=${this._parseDuration(this._hours)}
.min=${this._parseDuration(this._minutes)}
.sec=${this._parseDuration(this._seconds)}
.millisec=${this._parseDurationMillisec(this._milliseconds)}
@hour-changed=${this._hourChanged}
@min-changed=${this._minChanged}
@sec-changed=${this._secChanged}
@millisec-changed=${this._millisecChanged}
float-input-labels
no-hours-limit
always-float-input-labels
hour-label="hh"
min-label="mm"
sec-label="ss"
millisec-label="ms"
></paper-time-input>
`;
}
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;
}
}

View File

@ -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`
<ha-time-input
<ha-duration-input
.label=${this.label}
.required=${this.schema.required}
.data=${this.data}
></ha-time-input>
></ha-duration-input>
`;
}
}

View File

@ -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[];

View File

@ -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`
<paper-time-input
.label=${this.label}
.hour=${hours &&
(useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours)}
.min=${parts[1]}
.sec=${parts[2]}
.format=${useAMPM ? 12 : 24}
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
<ha-time-input
.value=${this.value}
.locale=${this.hass.locale}
.disabled=${this.disabled}
@change=${this._timeChanged}
@am-pm-changed=${this._timeChanged}
hide-label
enable-second
></paper-time-input>
></ha-time-input>
`;
}
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 {

View File

@ -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`
<paper-time-input
.label=${this.label}
.required=${this.required}
.autoValidate=${this.required}
error-message="Required"
enable-second
.enableMillisecond=${this.enableMillisecond}
format="24"
.hour=${this._parseDuration(this._hours)}
.min=${this._parseDuration(this._minutes)}
.sec=${this._parseDuration(this._seconds)}
.millisec=${this._parseDurationMillisec(this._milliseconds)}
@hour-changed=${this._hourChanged}
@min-changed=${this._minChanged}
@sec-changed=${this._secChanged}
@millisec-changed=${this._millisecChanged}
float-input-labels
no-hours-limit
always-float-input-labels
hour-label="hh"
min-label="mm"
sec-label="ss"
millisec-label="ms"
.hour=${hours}
.min=${parts[1]}
.sec=${parts[2]}
.format=${useAMPM ? 12 : 24}
.amPm=${useAMPM && (numberHours >= 12 ? "PM" : "AM")}
.disabled=${this.disabled}
@change=${this._timeChanged}
@am-pm-changed=${this._timeChanged}
.hideLabel=${this.hideLabel}
.enableSecond=${this.enableSecond}
></paper-time-input>
`;
}
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,
});
}
}

View File

@ -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 {

View File

@ -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`
<div class$="[[computeClassNames(stateObj)]]">
<template is="dom-if" if="[[doesHaveDate(stateObj)]]" restamp="">
<div>
<ha-date-input
id="dateInput"
label="Date"
value="{{selectedDate}}"
></ha-date-input>
</div>
</template>
<template is="dom-if" if="[[doesHaveTime(stateObj)]]" restamp="">
<div>
<paper-time-input
hour="{{selectedHour}}"
min="{{selectedMinute}}"
format="24"
></paper-time-input>
</div>
</template>
</div>
`;
}
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);

View File

@ -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`
<ha-date-input
.value=${`${this.stateObj.attributes.year}-${this.stateObj.attributes.month}-${this.stateObj.attributes.day}`}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
@value-changed=${this._dateChanged}
>
</ha-date-input>
`
: ``
}
${
this.stateObj.attributes.has_time
? html`
<ha-time-input
.value=${this.stateObj.state === UNKNOWN
? ""
: this.stateObj.attributes.has_date
? this.stateObj.state.split(" ")[1]
: this.stateObj.state}
.locale=${this.hass.locale}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
hide-label
@value-changed=${this._timeChanged}
@click=${this._stopEventPropagation}
></ha-time-input>
`
: ``
}
</hui-generic-entity-row>
`;
}
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;
}
}

View File

@ -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`<ha-time-input
return html`<ha-duration-input
.data=${this._timeData}
enableMillisecond
@value-changed=${this._valueChanged}
></ha-time-input>`;
></ha-duration-input>`;
}
private _valueChanged(ev: CustomEvent) {

View File

@ -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`
<ha-entity-picker
@ -67,14 +53,14 @@ export class HaStateCondition extends LitElement implements ConditionElement {
.value=${state}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
<ha-duration-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
)}
.name=${"for"}
.value=${forTime}
.data=${forTime}
@value-changed=${this._valueChanged}
></paper-input>
></ha-duration-input>
`;
}

View File

@ -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
></ha-entity-picker>`
: html`<paper-input
: html`<ha-time-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.time.after"
)}
name="after"
.locale=${this.hass.locale}
.name=${"after"}
.value=${after?.startsWith("input_datetime.") ? "" : after}
@value-changed=${this._valueChanged}
></paper-input>`}
></ha-time-input>`}
<ha-formfield
.label=${this.hass!.localize(
@ -134,14 +135,15 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
.hass=${this.hass}
allow-custom-entity
></ha-entity-picker>`
: html`<paper-input
: html`<ha-time-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.time.before"
)}
name="before"
.name=${"before"}
.locale=${this.hass.locale}
.value=${before?.startsWith("input_datetime.") ? "" : before}
@value-changed=${this._valueChanged}
></paper-input>`}
></ha-time-input>`}
${Object.keys(DAYS).map(
(day) => html`
<ha-formfield

View File

@ -9,6 +9,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
@ -80,6 +81,8 @@ export default class HaAutomationTriggerRow extends LitElement {
@property() public trigger!: Trigger;
@state() private _warnings?: string[];
@state() private _yamlMode = false;
protected render() {
@ -118,6 +121,20 @@ export default class HaAutomationTriggerRow extends LitElement {
</mwc-list-item>
</ha-button-menu>
</div>
${this._warnings
? html`<div class="warning">
${this.hass.localize("ui.errors.config.editor_not_supported")}:
<br />
${this._warnings.length && this._warnings[0] !== undefined
? html` <ul>
${this._warnings.map(
(warning) => html`<li>${warning}</li>`
)}
</ul>`
: ""}
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
</div>`
: ""}
${yamlMode
? html`
${selected === -1
@ -170,7 +187,7 @@ export default class HaAutomationTriggerRow extends LitElement {
@value-changed=${this._idChanged}
>
</paper-input>
<div>
<div @ui-mode-not-available=${this._handleUiModeNotAvailable}>
${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<ActionDetail>) {
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;
}

View File

@ -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`
<ha-entity-picker
.value="${entity_id}"
@ -82,14 +85,14 @@ export default class HaNumericStateTrigger extends LitElement {
@value-changed=${this._valueChanged}
dir="ltr"
></paper-textarea>
<paper-input
<ha-duration-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
)}
name="for"
.value=${trgFor}
.name=${"for"}
.data=${trgFor}
@value-changed=${this._valueChanged}
></paper-input>
></ha-duration-input>
`;
}

View File

@ -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`
<ha-entity-picker
@ -75,14 +77,14 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
.value=${to}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
<ha-duration-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
)}
.name=${"for"}
.value=${trgFor}
.data=${trgFor}
@value-changed=${this._valueChanged}
></paper-input>
></ha-duration-input>
`;
}

View File

@ -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`
<ha-formfield
const at = this.trigger.at;
if (Array.isArray(at)) {
return html``;
}
const inputMode =
this._inputMode ??
(at?.startsWith("input_datetime.") || at?.startsWith("sensor."));
return html`<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.automation.editor.triggers.type.time.type_value"
)}
@ -53,6 +74,7 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
?checked=${inputMode}
></ha-radio>
</ha-formfield>
${inputMode
? html`<ha-entity-picker
.label=${this.hass.localize(
@ -60,20 +82,26 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
)}
.includeDomains=${includeDomains}
.name=${"at"}
.value=${at?.startsWith("input_datetime.") ? at : ""}
.value=${at?.startsWith("input_datetime.") ||
at?.startsWith("sensor.")
? at
: ""}
@value-changed=${this._valueChanged}
.hass=${this.hass}
allow-custom-entity
></ha-entity-picker>`
: html`<paper-input
: html`<ha-time-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.time.at"
)}
name="at"
.value=${at?.startsWith("input_datetime.") ? "" : at}
.name=${"at"}
.value=${at?.startsWith("input_datetime.") ||
at?.startsWith("sensor.")
? ""
: at}
.locale=${this.hass.locale}
@value-changed=${this._valueChanged}
></paper-input>`}
`;
></ha-time-input>`} `;
}
private _handleModeChanged(ev: Event) {

View File

@ -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 {
<ha-date-input
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
.value=${`${stateObj.attributes.year}-${stateObj.attributes.month}-${stateObj.attributes.day}`}
@value-changed=${this._selectedValueChanged}
@value-changed=${this._dateChanged}
>
</ha-date-input>
${stateObj.attributes.has_time ? "," : ""}
`
: ``}
${stateObj.attributes.has_time
? html`
<paper-time-input
<ha-time-input
.value=${stateObj.state === UNKNOWN
? ""
: stateObj.attributes.has_date
? stateObj.state.split(" ")[1]
: stateObj.state}
.locale=${this.hass.locale}
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
.hour=${stateObj.state === UNKNOWN
? ""
: ("0" + stateObj.attributes.hour).slice(-2)}
.min=${stateObj.state === UNKNOWN
? ""
: ("0" + stateObj.attributes.minute).slice(-2)}
@change=${this._selectedValueChanged}
@click=${this._stopEventPropagation}
hide-label
.format=${24}
></paper-time-input>
@value-changed=${this._timeChanged}
@click=${this._stopEventPropagation}
></ha-time-input>
`
: ``}
</hui-generic-entity-row>
@ -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 {