diff --git a/src/common/const.ts b/src/common/const.ts index 4298e855be..203d0b1b38 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -211,6 +211,7 @@ export const DOMAINS_INPUT_ROW = [ "button", "cover", "date", + "datetime", "fan", "group", "humidifier", diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 72f1645337..a35b8cf780 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -117,59 +117,39 @@ export const computeStateDisplayFromEntityAttributes = ( const domain = computeDomain(entityId); + if (domain === "datetime") { + const time = new Date(state); + return formatDateTime(time, locale); + } + if (["date", "input_datetime", "time"].includes(domain)) { - if (state !== undefined) { - // If trying to display an explicit state, need to parse the explicit state to `Date` then format. - // Attributes aren't available, we have to use `state`. - try { - const components = state.split(" "); - if (components.length === 2) { - // Date and time. - return formatDateTime(new Date(components.join("T")), locale); + // If trying to display an explicit state, need to parse the explicit state to `Date` then format. + // Attributes aren't available, we have to use `state`. + try { + const components = state.split(" "); + if (components.length === 2) { + // Date and time. + return formatDateTime(new Date(components.join("T")), locale); + } + if (components.length === 1) { + if (state.includes("-")) { + // Date only. + return formatDate(new Date(`${state}T00:00`), locale); } - if (components.length === 1) { - if (state.includes("-")) { - // Date only. - return formatDate(new Date(`${state}T00:00`), locale); - } - if (state.includes(":")) { - // Time only. - const now = new Date(); - return formatTime( - new Date(`${now.toISOString().split("T")[0]}T${state}`), - locale - ); - } + if (state.includes(":")) { + // Time only. + const now = new Date(); + return formatTime( + new Date(`${now.toISOString().split("T")[0]}T${state}`), + locale + ); } - return state; - } catch (_e) { - // Formatting methods may throw error if date parsing doesn't go well, - // just return the state string in that case. - return state; - } - } else { - // If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format. - let date: Date; - if (attributes.has_date && attributes.has_time) { - date = new Date( - attributes.year, - attributes.month - 1, - attributes.day, - attributes.hour, - attributes.minute - ); - return formatDateTime(date, locale); - } - if (attributes.has_date) { - date = new Date(attributes.year, attributes.month - 1, attributes.day); - return formatDate(date, locale); - } - if (attributes.has_time) { - date = new Date(); - date.setHours(attributes.hour, attributes.minute); - return formatTime(date, locale); } return state; + } catch (_e) { + // Formatting methods may throw error if date parsing doesn't go well, + // just return the state string in that case. + return state; } } diff --git a/src/data/date.ts b/src/data/date.ts index 196ddcc2af..4ac5d9bf15 100644 --- a/src/data/date.ts +++ b/src/data/date.ts @@ -10,5 +10,5 @@ export const setDateValue = ( date: string | undefined = undefined ) => { const param = { entity_id: entityId, date }; - hass.callService(entityId.split(".", 1)[0], "set_value", param); + hass.callService("date", "set_value", param); }; diff --git a/src/data/datetime.ts b/src/data/datetime.ts new file mode 100644 index 0000000000..239bb0051a --- /dev/null +++ b/src/data/datetime.ts @@ -0,0 +1,12 @@ +import { HomeAssistant } from "../types"; + +export const setDateTimeValue = ( + hass: HomeAssistant, + entityId: string, + datetime: Date +) => { + hass.callService("datetime", "set_value", { + entity_id: entityId, + datetime: datetime.toISOString(), + }); +}; diff --git a/src/data/input_datetime.ts b/src/data/input_datetime.ts index 08af492afd..5a5d0182c6 100644 --- a/src/data/input_datetime.ts +++ b/src/data/input_datetime.ts @@ -38,7 +38,7 @@ export const setInputDateTimeValue = ( date: string | undefined = undefined ) => { const param = { entity_id: entityId, time, date }; - hass.callService(entityId.split(".", 1)[0], "set_datetime", param); + hass.callService("input_datetime", "set_datetime", param); }; export const fetchInputDateTime = (hass: HomeAssistant) => diff --git a/src/data/time.ts b/src/data/time.ts index c2e9310633..7bc0e85172 100644 --- a/src/data/time.ts +++ b/src/data/time.ts @@ -6,5 +6,5 @@ export const setTimeValue = ( time: string | undefined = undefined ) => { const param = { entity_id: entityId, time: time }; - hass.callService(entityId.split(".", 1)[0], "set_value", param); + hass.callService("time", "set_value", param); }; diff --git a/src/panels/lovelace/create-element/create-row-element.ts b/src/panels/lovelace/create-element/create-row-element.ts index 360b7023da..620ddb4660 100644 --- a/src/panels/lovelace/create-element/create-row-element.ts +++ b/src/panels/lovelace/create-element/create-row-element.ts @@ -28,6 +28,7 @@ const LAZY_LOAD_TYPES = { "climate-entity": () => import("../entity-rows/hui-climate-entity-row"), "cover-entity": () => import("../entity-rows/hui-cover-entity-row"), "date-entity": () => import("../entity-rows/hui-date-entity-row"), + "datetime-entity": () => import("../entity-rows/hui-datetime-entity-row"), "group-entity": () => import("../entity-rows/hui-group-entity-row"), "input-button-entity": () => import("../entity-rows/hui-input-button-entity-row"), @@ -63,6 +64,7 @@ const DOMAIN_TO_ELEMENT_TYPE = { climate: "climate", cover: "cover", date: "date", + datetime: "datetime", fan: "toggle", group: "group", humidifier: "humidifier", diff --git a/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts b/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts new file mode 100644 index 0000000000..bf6b2845e8 --- /dev/null +++ b/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts @@ -0,0 +1,120 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../components/ha-date-input"; +import { format } from "date-fns"; +import { isUnavailableState } from "../../../data/entity"; +import { setDateTimeValue } from "../../../data/datetime"; +import type { HomeAssistant } from "../../../types"; +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-datetime-entity-row") +class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: EntityConfig; + + public setConfig(config: EntityConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + return hasConfigOrEntityChanged(this, changedProps); + } + + protected render(): TemplateResult | typeof nothing { + if (!this._config || !this.hass) { + return nothing; + } + + const stateObj = this.hass.states[this._config.entity]; + + if (!stateObj) { + return html` + + ${createEntityNotFoundWarning(this.hass, this._config.entity)} + + `; + } + + const dateObj = new Date(stateObj.state); + const time = format(dateObj, "HH:mm:ss"); + const date = format(dateObj, "yyyy-MM-dd"); + + return html` + + + + + + `; + } + + private _stopEventPropagation(ev: Event): void { + ev.stopPropagation(); + } + + private _timeChanged(ev: CustomEvent<{ value: string }>): void { + const stateObj = this.hass!.states[this._config!.entity]; + const dateObj = new Date(stateObj.state); + const newTime = ev.detail.value.split(":").map(Number); + dateObj.setHours(newTime[0], newTime[1], newTime[2]); + + setDateTimeValue(this.hass!, stateObj.entity_id, dateObj); + } + + private _dateChanged(ev: CustomEvent<{ value: string }>): void { + const stateObj = this.hass!.states[this._config!.entity]; + const dateObj = new Date(stateObj.state); + const newDate = ev.detail.value.split("-").map(Number); + dateObj.setFullYear(newDate[0], newDate[1] - 1, newDate[2]); + + setDateTimeValue(this.hass!, stateObj.entity_id, dateObj); + } + + static get styles(): CSSResultGroup { + return css` + ha-date-input + ha-time-input { + margin-left: 4px; + margin-inline-start: 4px; + margin-inline-end: initial; + direction: var(--direction); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-datetime-entity-row": HuiInputDatetimeEntityRow; + } +}