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 { html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "../ha-time-input"; import "../ha-duration-input";
import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./ha-form"; import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./ha-form";
@customElement("ha-form-positive_time_period_dict") @customElement("ha-form-positive_time_period_dict")
@ -23,11 +23,11 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-time-input <ha-duration-input
.label=${this.label} .label=${this.label}
.required=${this.schema.required} .required=${this.schema.required}
.data=${this.data} .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 { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event"; 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-boolean";
import "./ha-form-constant"; import "./ha-form-constant";
import "./ha-form-float"; import "./ha-form-float";
@ -88,7 +88,7 @@ export type HaFormFloatData = number;
export type HaFormBooleanData = boolean; export type HaFormBooleanData = boolean;
export type HaFormSelectData = string; export type HaFormSelectData = string;
export type HaFormMultiSelectData = string[]; export type HaFormMultiSelectData = string[];
export type HaFormTimeData = HaTimeData; export type HaFormTimeData = HaDurationData;
export interface HaFormElement extends LitElement { export interface HaFormElement extends LitElement {
schema: HaFormSchema | HaFormSchema[]; schema: HaFormSchema | HaFormSchema[];

View File

@ -1,10 +1,8 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; 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 { TimeSelector } from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../paper-time-input"; import "../ha-time-input";
@customElement("ha-selector-time") @customElement("ha-selector-time")
export class HaTimeSelector extends LitElement { export class HaTimeSelector extends LitElement {
@ -19,46 +17,16 @@ export class HaTimeSelector extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
const useAMPM = useAmPm(this.hass.locale);
const parts = this.value?.split(":") || [];
const hours = parts[0];
return html` return html`
<paper-time-input <ha-time-input
.label=${this.label} .value=${this.value}
.hour=${hours && .locale=${this.hass.locale}
(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")}
.disabled=${this.disabled} .disabled=${this.disabled}
@change=${this._timeChanged}
@am-pm-changed=${this._timeChanged}
hide-label hide-label
enable-second 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 { declare global {

View File

@ -1,134 +1,78 @@
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { useAmPm } from "../common/datetime/use_am_pm";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import "./paper-time-input"; import "./paper-time-input";
import { FrontendLocaleData } from "../data/translation";
export interface HaTimeData {
hours?: number;
minutes?: number;
seconds?: number;
milliseconds?: number;
}
@customElement("ha-time-input") @customElement("ha-time-input")
class HaTimeInput extends LitElement { export class HaTimeInput extends LitElement {
@property() public data!: HaTimeData; @property() public locale!: FrontendLocaleData;
@property() public value?: string;
@property() public label?: 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() { const parts = this.value?.split(":") || [];
if (this._input) { let hours = parts[0];
this._input.focus(); 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` return html`
<paper-time-input <paper-time-input
.label=${this.label} .label=${this.label}
.required=${this.required} .hour=${hours}
.autoValidate=${this.required} .min=${parts[1]}
error-message="Required" .sec=${parts[2]}
enable-second .format=${useAMPM ? 12 : 24}
.enableMillisecond=${this.enableMillisecond} .amPm=${useAMPM && (numberHours >= 12 ? "PM" : "AM")}
format="24" .disabled=${this.disabled}
.hour=${this._parseDuration(this._hours)} @change=${this._timeChanged}
.min=${this._parseDuration(this._minutes)} @am-pm-changed=${this._timeChanged}
.sec=${this._parseDuration(this._seconds)} .hideLabel=${this.hideLabel}
.millisec=${this._parseDurationMillisec(this._milliseconds)} .enableSecond=${this.enableSecond}
@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> ></paper-time-input>
`; `;
} }
private get _hours() { private _timeChanged(ev) {
return this.data && this.data.hours ? Number(this.data.hours) : 0; let value = ev.target.value;
} const useAMPM = useAmPm(this.locale);
let hours = Number(ev.target.hour || 0);
private get _minutes() { if (value && useAMPM) {
return this.data && this.data.minutes ? Number(this.data.minutes) : 0; if (ev.target.amPm === "PM" && hours < 12) {
} hours += 12;
}
private get _seconds() { if (ev.target.amPm === "AM" && hours === 12) {
return this.data && this.data.seconds ? Number(this.data.seconds) : 0; hours = 0;
} }
value = `${hours.toString().padStart(2, "0")}:${ev.target.min || "00"}:${
private get _milliseconds() { ev.target.sec || "00"
return this.data && this.data.milliseconds }`;
? Number(this.data.milliseconds) }
: 0; if (value === this.value) {
}
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; return;
} }
this.value = value;
let hours = this._hours; fireEvent(this, "change");
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", { fireEvent(this, "value-changed", {
value: { value,
hours,
minutes,
seconds: this._seconds,
milliseconds: this._milliseconds,
...{ [unit]: value },
},
}); });
} }
} }

View File

@ -46,9 +46,11 @@ export interface BlueprintAutomationConfig extends ManualAutomationConfig {
} }
export interface ForDict { export interface ForDict {
hours?: number | string; days?: number;
minutes?: number | string; hours?: number;
seconds?: number | string; minutes?: number;
seconds?: number;
milliseconds?: number;
} }
export interface ContextConstraint { 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 { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template"; import { hasTemplate } from "../../../../../common/string/has-template";
import "../../../../../components/entity/ha-entity-picker"; import type { HaDurationData } from "../../../../../components/ha-duration-input";
import { HaFormTimeData } from "../../../../../components/ha-form/ha-form"; import "../../../../../components/ha-duration-input";
import "../../../../../components/ha-service-picker";
import { DelayAction } from "../../../../../data/script"; import { DelayAction } from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { ActionElement } from "../ha-automation-action-row"; import { ActionElement } from "../ha-automation-action-row";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
@customElement("ha-automation-action-delay") @customElement("ha-automation-action-delay")
export class HaDelayAction extends LitElement implements ActionElement { export class HaDelayAction extends LitElement implements ActionElement {
@ -16,13 +15,13 @@ export class HaDelayAction extends LitElement implements ActionElement {
@property() public action!: DelayAction; @property() public action!: DelayAction;
@property() public _timeData!: HaFormTimeData; @property() public _timeData!: HaDurationData;
public static get defaultConfig() { public static get defaultConfig() {
return { delay: "" }; return { delay: "" };
} }
protected updated(changedProperties: PropertyValues) { public willUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("action")) { if (!changedProperties.has("action")) {
return; return;
} }
@ -36,37 +35,15 @@ export class HaDelayAction extends LitElement implements ActionElement {
return; return;
} }
if (typeof this.action.delay !== "object") { this._timeData = createDurationData(this.action.delay);
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,
};
} }
protected render() { protected render() {
return html`<ha-time-input return html`<ha-duration-input
.data=${this._timeData} .data=${this._timeData}
enableMillisecond enableMillisecond
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-time-input>`; ></ha-duration-input>`;
} }
private _valueChanged(ev: CustomEvent) { private _valueChanged(ev: CustomEvent) {

View File

@ -1,14 +1,16 @@
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; 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-attribute-picker";
import "../../../../../components/entity/ha-entity-picker"; import "../../../../../components/entity/ha-entity-picker";
import { ForDict, StateCondition } from "../../../../../data/automation"; import { StateCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { import {
ConditionElement, ConditionElement,
handleChangeEvent, handleChangeEvent,
} from "../ha-automation-condition-row"; } from "../ha-automation-condition-row";
import "../../../../../components/ha-duration-input";
@customElement("ha-automation-condition-state") @customElement("ha-automation-condition-state")
export class HaStateCondition extends LitElement implements ConditionElement { export class HaStateCondition extends LitElement implements ConditionElement {
@ -22,23 +24,7 @@ export class HaStateCondition extends LitElement implements ConditionElement {
protected render() { protected render() {
const { entity_id, attribute, state } = this.condition; const { entity_id, attribute, state } = this.condition;
let forTime = this.condition.for; const forTime = createDurationData(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}`;
}
return html` return html`
<ha-entity-picker <ha-entity-picker
@ -67,14 +53,14 @@ export class HaStateCondition extends LitElement implements ConditionElement {
.value=${state} .value=${state}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></paper-input>
<paper-input <ha-duration-input
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for" "ui.panel.config.automation.editor.triggers.type.state.for"
)} )}
.name=${"for"} .name=${"for"}
.value=${forTime} .data=${forTime}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></ha-duration-input>
`; `;
} }

View File

@ -1,5 +1,4 @@
import { Radio } from "@material/mwc-radio"; import { Radio } from "@material/mwc-radio";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
@ -13,6 +12,7 @@ import {
ConditionElement, ConditionElement,
handleChangeEvent, handleChangeEvent,
} from "../ha-automation-condition-row"; } from "../ha-automation-condition-row";
import "../../../../../components/ha-time-input";
const includeDomains = ["input_datetime"]; const includeDomains = ["input_datetime"];
@ -89,14 +89,15 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
.hass=${this.hass} .hass=${this.hass}
allow-custom-entity allow-custom-entity
></ha-entity-picker>` ></ha-entity-picker>`
: html`<paper-input : html`<ha-time-input
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.time.after" "ui.panel.config.automation.editor.conditions.type.time.after"
)} )}
name="after" .locale=${this.hass.locale}
.name=${"after"}
.value=${after?.startsWith("input_datetime.") ? "" : after} .value=${after?.startsWith("input_datetime.") ? "" : after}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input>`} ></ha-time-input>`}
<ha-formfield <ha-formfield
.label=${this.hass!.localize( .label=${this.hass!.localize(
@ -134,14 +135,15 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
.hass=${this.hass} .hass=${this.hass}
allow-custom-entity allow-custom-entity
></ha-entity-picker>` ></ha-entity-picker>`
: html`<paper-input : html`<ha-time-input
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.time.before" "ui.panel.config.automation.editor.conditions.type.time.before"
)} )}
name="before" .name=${"before"}
.locale=${this.hass.locale}
.value=${before?.startsWith("input_datetime.") ? "" : before} .value=${before?.startsWith("input_datetime.") ? "" : before}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input>`} ></ha-time-input>`}
${Object.keys(DAYS).map( ${Object.keys(DAYS).map(
(day) => html` (day) => html`
<ha-formfield <ha-formfield

View File

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

View File

@ -1,11 +1,15 @@
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea"; import "@polymer/paper-input/paper-textarea";
import { html, LitElement } from "lit"; import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; 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 "../../../../../components/entity/ha-entity-picker";
import { ForDict, NumericStateTrigger } from "../../../../../data/automation"; import { NumericStateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row"; import { handleChangeEvent } from "../ha-automation-trigger-row";
import "../../../../../components/ha-duration-input";
@customElement("ha-automation-trigger-numeric_state") @customElement("ha-automation-trigger-numeric_state")
export default class HaNumericStateTrigger extends LitElement { export default class HaNumericStateTrigger extends LitElement {
@ -13,6 +17,20 @@ export default class HaNumericStateTrigger extends LitElement {
@property() public trigger!: NumericStateTrigger; @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() { public static get defaultConfig() {
return { return {
entity_id: "", entity_id: "",
@ -21,23 +39,8 @@ export default class HaNumericStateTrigger extends LitElement {
public render() { public render() {
const { value_template, entity_id, attribute, below, above } = this.trigger; 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` return html`
<ha-entity-picker <ha-entity-picker
.value="${entity_id}" .value="${entity_id}"
@ -82,14 +85,14 @@ export default class HaNumericStateTrigger extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
dir="ltr" dir="ltr"
></paper-textarea> ></paper-textarea>
<paper-input <ha-duration-input
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for" "ui.panel.config.automation.editor.triggers.type.state.for"
)} )}
name="for" .name=${"for"}
.value=${trgFor} .data=${trgFor}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></ha-duration-input>
`; `;
} }

View File

@ -1,10 +1,14 @@
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { html, LitElement } from "lit"; import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; 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-attribute-picker";
import "../../../../../components/entity/ha-entity-picker"; import "../../../../../components/entity/ha-entity-picker";
import { ForDict, StateTrigger } from "../../../../../data/automation"; import { StateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-duration-input";
import { import {
handleChangeEvent, handleChangeEvent,
TriggerElement, TriggerElement,
@ -20,25 +24,23 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
return { entity_id: "" }; 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() { protected render() {
const { entity_id, attribute, to, from } = this.trigger; const { entity_id, attribute, to, from } = 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` return html`
<ha-entity-picker <ha-entity-picker
@ -75,14 +77,14 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
.value=${to} .value=${to}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></paper-input>
<paper-input <ha-duration-input
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for" "ui.panel.config.automation.editor.triggers.type.state.for"
)} )}
.name=${"for"} .name=${"for"}
.value=${trgFor} .data=${trgFor}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></ha-duration-input>
`; `;
} }

View File

@ -1,5 +1,4 @@
import "@polymer/paper-input/paper-input"; import { html, LitElement, PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../../../components/entity/ha-entity-picker"; import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-formfield"; import "../../../../../components/ha-formfield";
@ -10,9 +9,10 @@ import {
handleChangeEvent, handleChangeEvent,
TriggerElement, TriggerElement,
} from "../ha-automation-trigger-row"; } from "../ha-automation-trigger-row";
import "../../../../../components/ha-time-input";
import { fireEvent } from "../../../../../common/dom/fire_event";
const includeDomains = ["input_datetime"]; const includeDomains = ["input_datetime"];
@customElement("ha-automation-trigger-time") @customElement("ha-automation-trigger-time")
export class HaTimeTrigger extends LitElement implements TriggerElement { export class HaTimeTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -25,11 +25,32 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
return { at: "" }; 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() { protected render() {
const { at } = this.trigger; const at = this.trigger.at;
const inputMode = this._inputMode ?? at?.startsWith("input_datetime.");
return html` if (Array.isArray(at)) {
<ha-formfield return html``;
}
const inputMode =
this._inputMode ??
(at?.startsWith("input_datetime.") || at?.startsWith("sensor."));
return html`<ha-formfield
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.config.automation.editor.triggers.type.time.type_value" "ui.panel.config.automation.editor.triggers.type.time.type_value"
)} )}
@ -53,6 +74,7 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
?checked=${inputMode} ?checked=${inputMode}
></ha-radio> ></ha-radio>
</ha-formfield> </ha-formfield>
${inputMode ${inputMode
? html`<ha-entity-picker ? html`<ha-entity-picker
.label=${this.hass.localize( .label=${this.hass.localize(
@ -60,20 +82,26 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
)} )}
.includeDomains=${includeDomains} .includeDomains=${includeDomains}
.name=${"at"} .name=${"at"}
.value=${at?.startsWith("input_datetime.") ? at : ""} .value=${at?.startsWith("input_datetime.") ||
at?.startsWith("sensor.")
? at
: ""}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.hass=${this.hass} .hass=${this.hass}
allow-custom-entity allow-custom-entity
></ha-entity-picker>` ></ha-entity-picker>`
: html`<paper-input : html`<ha-time-input
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.time.at" "ui.panel.config.automation.editor.triggers.type.time.at"
)} )}
name="at" .name=${"at"}
.value=${at?.startsWith("input_datetime.") ? "" : at} .value=${at?.startsWith("input_datetime.") ||
at?.startsWith("sensor.")
? ""
: at}
.locale=${this.hass.locale}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input>`} ></ha-time-input>`} `;
`;
} }
private _handleModeChanged(ev: Event) { private _handleModeChanged(ev: Event) {

View File

@ -1,9 +1,13 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import {
import { customElement, property, state, query } from "lit/decorators"; css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-date-input"; 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 { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
import { setInputDateTimeValue } from "../../../data/input_datetime"; import { setInputDateTimeValue } from "../../../data/input_datetime";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
@ -11,6 +15,7 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row"; import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { EntityConfig, LovelaceRow } from "./types"; import type { EntityConfig, LovelaceRow } from "./types";
import "../../../components/ha-time-input";
@customElement("hui-input-datetime-entity-row") @customElement("hui-input-datetime-entity-row")
class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
@ -18,10 +23,6 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
@state() private _config?: EntityConfig; @state() private _config?: EntityConfig;
@query("paper-time-input") private _timeInputEl?: PaperTimeInput;
@query("ha-date-input") private _dateInputEl?: HaDateInput;
public setConfig(config: EntityConfig): void { public setConfig(config: EntityConfig): void {
if (!config) { if (!config) {
throw new Error("Invalid configuration"); throw new Error("Invalid configuration");
@ -55,27 +56,25 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
<ha-date-input <ha-date-input
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)} .disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
.value=${`${stateObj.attributes.year}-${stateObj.attributes.month}-${stateObj.attributes.day}`} .value=${`${stateObj.attributes.year}-${stateObj.attributes.month}-${stateObj.attributes.day}`}
@value-changed=${this._selectedValueChanged} @value-changed=${this._dateChanged}
> >
</ha-date-input> </ha-date-input>
${stateObj.attributes.has_time ? "," : ""}
` `
: ``} : ``}
${stateObj.attributes.has_time ${stateObj.attributes.has_time
? html` ? 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)} .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 hide-label
.format=${24} @value-changed=${this._timeChanged}
></paper-time-input> @click=${this._stopEventPropagation}
></ha-time-input>
` `
: ``} : ``}
</hui-generic-entity-row> </hui-generic-entity-row>
@ -86,19 +85,37 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
ev.stopPropagation(); 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 stateObj = this.hass!.states[this._config!.entity];
const time = this._timeInputEl setInputDateTimeValue(
? this._timeInputEl.value?.trim() this.hass!,
: undefined; stateObj.entity_id,
stateObj.attributes.has_time ? stateObj.state.split(" ")[1] : undefined,
const date = this._dateInputEl ? this._dateInputEl.value : undefined; ev.detail.value
);
setInputDateTimeValue(this.hass!, stateObj.entity_id, time, date);
ev.target.blur(); ev.target.blur();
} }
static get styles(): CSSResultGroup {
return css`
ha-date-input + ha-time-input {
margin-left: 4px;
}
`;
}
} }
declare global { declare global {