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;
+ }
+}