diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto
index 17826ea7ed..b8073abc19 100644
--- a/esphome/components/api/api.proto
+++ b/esphome/components/api/api.proto
@@ -47,6 +47,7 @@ service APIConnection {
   rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
   rpc date_command (DateCommandRequest) returns (void) {}
   rpc time_command (TimeCommandRequest) returns (void) {}
+  rpc datetime_command (DateTimeCommandRequest) returns (void) {}
 
   rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
   rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
@@ -1777,3 +1778,40 @@ message ValveCommandRequest {
   float position = 3;
   bool stop = 4;
 }
+
+// ==================== DATETIME DATETIME ====================
+message ListEntitiesDateTimeResponse {
+  option (id) = 112;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_DATETIME_DATETIME";
+
+  string object_id = 1;
+  fixed32 key = 2;
+  string name = 3;
+  string unique_id = 4;
+
+  string icon = 5;
+  bool disabled_by_default = 6;
+  EntityCategory entity_category = 7;
+}
+message DateTimeStateResponse {
+  option (id) = 113;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_DATETIME_DATETIME";
+  option (no_delay) = true;
+
+  fixed32 key = 1;
+  // If the datetime does not have a valid state yet.
+  // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
+  bool missing_state = 2;
+  fixed32 epoch_seconds = 3;
+}
+message DateTimeCommandRequest {
+  option (id) = 114;
+  option (source) = SOURCE_CLIENT;
+  option (ifdef) = "USE_DATETIME_DATETIME";
+  option (no_delay) = true;
+
+  fixed32 key = 1;
+  fixed32 epoch_seconds = 2;
+}
diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index ec09604d95..b31212bbdb 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -772,6 +772,44 @@ void APIConnection::time_command(const TimeCommandRequest &msg) {
 }
 #endif
 
+#ifdef USE_DATETIME_DATETIME
+bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
+  if (!this->state_subscription_)
+    return false;
+
+  DateTimeStateResponse resp{};
+  resp.key = datetime->get_object_id_hash();
+  resp.missing_state = !datetime->has_state();
+  if (datetime->has_state()) {
+    ESPTime state = datetime->state_as_esptime();
+    resp.epoch_seconds = state.timestamp;
+  }
+  return this->send_date_time_state_response(resp);
+}
+bool APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) {
+  ListEntitiesDateTimeResponse msg;
+  msg.key = datetime->get_object_id_hash();
+  msg.object_id = datetime->get_object_id();
+  if (datetime->has_own_name())
+    msg.name = datetime->get_name();
+  msg.unique_id = get_default_unique_id("datetime", datetime);
+  msg.icon = datetime->get_icon();
+  msg.disabled_by_default = datetime->is_disabled_by_default();
+  msg.entity_category = static_cast<enums::EntityCategory>(datetime->get_entity_category());
+
+  return this->send_list_entities_date_time_response(msg);
+}
+void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
+  datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key);
+  if (datetime == nullptr)
+    return;
+
+  auto call = datetime->make_call();
+  call.set_datetime(msg.epoch_seconds);
+  call.perform();
+}
+#endif
+
 #ifdef USE_TEXT
 bool APIConnection::send_text_state(text::Text *text, std::string state) {
   if (!this->state_subscription_)
diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h
index 2c1d733d3e..ee466c5d10 100644
--- a/esphome/components/api/api_connection.h
+++ b/esphome/components/api/api_connection.h
@@ -82,6 +82,11 @@ class APIConnection : public APIServerConnection {
   bool send_time_info(datetime::TimeEntity *time);
   void time_command(const TimeCommandRequest &msg) override;
 #endif
+#ifdef USE_DATETIME_DATETIME
+  bool send_datetime_state(datetime::DateTimeEntity *datetime);
+  bool send_datetime_info(datetime::DateTimeEntity *datetime);
+  void datetime_command(const DateTimeCommandRequest &msg) override;
+#endif
 #ifdef USE_TEXT
   bool send_text_state(text::Text *text, std::string state);
   bool send_text_info(text::Text *text);
diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp
index 3f01d88c58..6ec1870d72 100644
--- a/esphome/components/api/api_pb2.cpp
+++ b/esphome/components/api/api_pb2.cpp
@@ -8093,6 +8093,179 @@ void ValveCommandRequest::dump_to(std::string &out) const {
   out.append("}");
 }
 #endif
+bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 6: {
+      this->disabled_by_default = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->entity_category = value.as_enum<enums::EntityCategory>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesDateTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 5: {
+      this->icon = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesDateTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_string(5, this->icon);
+  buffer.encode_bool(6, this->disabled_by_default);
+  buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
+}
+#ifdef HAS_PROTO_MESSAGE_DUMP
+void ListEntitiesDateTimeResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("ListEntitiesDateTimeResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  disabled_by_default: ");
+  out.append(YESNO(this->disabled_by_default));
+  out.append("\n");
+
+  out.append("  entity_category: ");
+  out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
+  out.append("\n");
+  out.append("}");
+}
+#endif
+bool DateTimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->missing_state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool DateTimeStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 3: {
+      this->epoch_seconds = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->missing_state);
+  buffer.encode_fixed32(3, this->epoch_seconds);
+}
+#ifdef HAS_PROTO_MESSAGE_DUMP
+void DateTimeStateResponse::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("DateTimeStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+
+  out.append("  epoch_seconds: ");
+  sprintf(buffer, "%" PRIu32, this->epoch_seconds);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+#endif
+bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 2: {
+      this->epoch_seconds = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void DateTimeCommandRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_fixed32(2, this->epoch_seconds);
+}
+#ifdef HAS_PROTO_MESSAGE_DUMP
+void DateTimeCommandRequest::dump_to(std::string &out) const {
+  __attribute__((unused)) char buffer[64];
+  out.append("DateTimeCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%" PRIu32, this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  epoch_seconds: ");
+  sprintf(buffer, "%" PRIu32, this->epoch_seconds);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+#endif
 
 }  // namespace api
 }  // namespace esphome
diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h
index 9a6aab254d..14fd95df37 100644
--- a/esphome/components/api/api_pb2.h
+++ b/esphome/components/api/api_pb2.h
@@ -2060,6 +2060,51 @@ class ValveCommandRequest : public ProtoMessage {
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
+class ListEntitiesDateTimeResponse : public ProtoMessage {
+ public:
+  std::string object_id{};
+  uint32_t key{0};
+  std::string name{};
+  std::string unique_id{};
+  std::string icon{};
+  bool disabled_by_default{false};
+  enums::EntityCategory entity_category{};
+  void encode(ProtoWriteBuffer buffer) const override;
+#ifdef HAS_PROTO_MESSAGE_DUMP
+  void dump_to(std::string &out) const override;
+#endif
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class DateTimeStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};
+  bool missing_state{false};
+  uint32_t epoch_seconds{0};
+  void encode(ProtoWriteBuffer buffer) const override;
+#ifdef HAS_PROTO_MESSAGE_DUMP
+  void dump_to(std::string &out) const override;
+#endif
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class DateTimeCommandRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};
+  uint32_t epoch_seconds{0};
+  void encode(ProtoWriteBuffer buffer) const override;
+#ifdef HAS_PROTO_MESSAGE_DUMP
+  void dump_to(std::string &out) const override;
+#endif
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+};
 
 }  // namespace api
 }  // namespace esphome
diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp
index ced81fa643..093fe917e0 100644
--- a/esphome/components/api/api_pb2_service.cpp
+++ b/esphome/components/api/api_pb2_service.cpp
@@ -591,6 +591,24 @@ bool APIServerConnectionBase::send_valve_state_response(const ValveStateResponse
 #endif
 #ifdef USE_VALVE
 #endif
+#ifdef USE_DATETIME_DATETIME
+bool APIServerConnectionBase::send_list_entities_date_time_response(const ListEntitiesDateTimeResponse &msg) {
+#ifdef HAS_PROTO_MESSAGE_DUMP
+  ESP_LOGVV(TAG, "send_list_entities_date_time_response: %s", msg.dump().c_str());
+#endif
+  return this->send_message_<ListEntitiesDateTimeResponse>(msg, 112);
+}
+#endif
+#ifdef USE_DATETIME_DATETIME
+bool APIServerConnectionBase::send_date_time_state_response(const DateTimeStateResponse &msg) {
+#ifdef HAS_PROTO_MESSAGE_DUMP
+  ESP_LOGVV(TAG, "send_date_time_state_response: %s", msg.dump().c_str());
+#endif
+  return this->send_message_<DateTimeStateResponse>(msg, 113);
+}
+#endif
+#ifdef USE_DATETIME_DATETIME
+#endif
 bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
   switch (msg_type) {
     case 1: {
@@ -1064,6 +1082,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
       ESP_LOGVV(TAG, "on_valve_command_request: %s", msg.dump().c_str());
 #endif
       this->on_valve_command_request(msg);
+#endif
+      break;
+    }
+    case 114: {
+#ifdef USE_DATETIME_DATETIME
+      DateTimeCommandRequest msg;
+      msg.decode(msg_data, msg_size);
+#ifdef HAS_PROTO_MESSAGE_DUMP
+      ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str());
+#endif
+      this->on_date_time_command_request(msg);
 #endif
       break;
     }
@@ -1379,6 +1408,19 @@ void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg)
   this->time_command(msg);
 }
 #endif
+#ifdef USE_DATETIME_DATETIME
+void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->datetime_command(msg);
+}
+#endif
 #ifdef USE_BLUETOOTH_PROXY
 void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
     const SubscribeBluetoothLEAdvertisementsRequest &msg) {
diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h
index c8b2bc5789..196d904aca 100644
--- a/esphome/components/api/api_pb2_service.h
+++ b/esphome/components/api/api_pb2_service.h
@@ -294,6 +294,15 @@ class APIServerConnectionBase : public ProtoService {
 #endif
 #ifdef USE_VALVE
   virtual void on_valve_command_request(const ValveCommandRequest &value){};
+#endif
+#ifdef USE_DATETIME_DATETIME
+  bool send_list_entities_date_time_response(const ListEntitiesDateTimeResponse &msg);
+#endif
+#ifdef USE_DATETIME_DATETIME
+  bool send_date_time_state_response(const DateTimeStateResponse &msg);
+#endif
+#ifdef USE_DATETIME_DATETIME
+  virtual void on_date_time_command_request(const DateTimeCommandRequest &value){};
 #endif
  protected:
   bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
@@ -358,6 +367,9 @@ class APIServerConnection : public APIServerConnectionBase {
 #ifdef USE_DATETIME_TIME
   virtual void time_command(const TimeCommandRequest &msg) = 0;
 #endif
+#ifdef USE_DATETIME_DATETIME
+  virtual void datetime_command(const DateTimeCommandRequest &msg) = 0;
+#endif
 #ifdef USE_BLUETOOTH_PROXY
   virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
 #endif
@@ -453,6 +465,9 @@ class APIServerConnection : public APIServerConnectionBase {
 #ifdef USE_DATETIME_TIME
   void on_time_command_request(const TimeCommandRequest &msg) override;
 #endif
+#ifdef USE_DATETIME_DATETIME
+  void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
+#endif
 #ifdef USE_BLUETOOTH_PROXY
   void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
 #endif
diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp
index 6d4e4db1e8..0725547771 100644
--- a/esphome/components/api/api_server.cpp
+++ b/esphome/components/api/api_server.cpp
@@ -273,6 +273,15 @@ void APIServer::on_time_update(datetime::TimeEntity *obj) {
 }
 #endif
 
+#ifdef USE_DATETIME_DATETIME
+void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) {
+  if (obj->is_internal())
+    return;
+  for (auto &c : this->clients_)
+    c->send_datetime_state(obj);
+}
+#endif
+
 #ifdef USE_TEXT
 void APIServer::on_text_update(text::Text *obj, const std::string &state) {
   if (obj->is_internal())
diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h
index e9e03cde0d..2e1fbdf67c 100644
--- a/esphome/components/api/api_server.h
+++ b/esphome/components/api/api_server.h
@@ -72,6 +72,9 @@ class APIServer : public Component, public Controller {
 #ifdef USE_DATETIME_TIME
   void on_time_update(datetime::TimeEntity *obj) override;
 #endif
+#ifdef USE_DATETIME_DATETIME
+  void on_datetime_update(datetime::DateTimeEntity *obj) override;
+#endif
 #ifdef USE_TEXT
   void on_text_update(text::Text *obj, const std::string &state) override;
 #endif
diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp
index 82bfd45333..a7dbf9a6e7 100644
--- a/esphome/components/api/list_entities.cpp
+++ b/esphome/components/api/list_entities.cpp
@@ -71,6 +71,12 @@ bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { return this->cl
 bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_info(time); }
 #endif
 
+#ifdef USE_DATETIME_DATETIME
+bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) {
+  return this->client_->send_datetime_info(datetime);
+}
+#endif
+
 #ifdef USE_TEXT
 bool ListEntitiesIterator::on_text(text::Text *text) { return this->client_->send_text_info(text); }
 #endif
diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h
index 19cd99ea01..c1fd8b82c4 100644
--- a/esphome/components/api/list_entities.h
+++ b/esphome/components/api/list_entities.h
@@ -52,6 +52,9 @@ class ListEntitiesIterator : public ComponentIterator {
 #ifdef USE_DATETIME_TIME
   bool on_time(datetime::TimeEntity *time) override;
 #endif
+#ifdef USE_DATETIME_DATETIME
+  bool on_datetime(datetime::DateTimeEntity *datetime) override;
+#endif
 #ifdef USE_TEXT
   bool on_text(text::Text *text) override;
 #endif
diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp
index 7aa8e8ffac..005ab0e6da 100644
--- a/esphome/components/api/subscribe_state.cpp
+++ b/esphome/components/api/subscribe_state.cpp
@@ -48,6 +48,11 @@ bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->cl
 #ifdef USE_DATETIME_TIME
 bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); }
 #endif
+#ifdef USE_DATETIME_DATETIME
+bool InitialStateIterator::on_datetime(datetime::DateTimeEntity *datetime) {
+  return this->client_->send_datetime_state(datetime);
+}
+#endif
 #ifdef USE_TEXT
 bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text, text->state); }
 #endif
diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h
index 17d444c441..8c725e422e 100644
--- a/esphome/components/api/subscribe_state.h
+++ b/esphome/components/api/subscribe_state.h
@@ -49,6 +49,9 @@ class InitialStateIterator : public ComponentIterator {
 #ifdef USE_DATETIME_TIME
   bool on_time(datetime::TimeEntity *time) override;
 #endif
+#ifdef USE_DATETIME_DATETIME
+  bool on_datetime(datetime::DateTimeEntity *datetime) override;
+#endif
 #ifdef USE_TEXT
   bool on_text(text::Text *text) override;
 #endif
diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py
index a22c60aae9..639a035159 100644
--- a/esphome/components/datetime/__init__.py
+++ b/esphome/components/datetime/__init__.py
@@ -1,6 +1,5 @@
 import esphome.codegen as cg
 
-# import cpp_generator as cpp
 import esphome.config_validation as cv
 from esphome import automation
 from esphome.components import mqtt, time
@@ -13,6 +12,7 @@ from esphome.const import (
     CONF_TYPE,
     CONF_MQTT_ID,
     CONF_DATE,
+    CONF_DATETIME,
     CONF_TIME,
     CONF_YEAR,
     CONF_MONTH,
@@ -27,6 +27,7 @@ from esphome.cpp_helpers import setup_entity
 
 
 CODEOWNERS = ["@rfdarter", "@jesserockz"]
+DEPENDENCIES = ["time"]
 
 IS_PLATFORM_COMPONENT = True
 
@@ -34,10 +35,12 @@ datetime_ns = cg.esphome_ns.namespace("datetime")
 DateTimeBase = datetime_ns.class_("DateTimeBase", cg.EntityBase)
 DateEntity = datetime_ns.class_("DateEntity", DateTimeBase)
 TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase)
+DateTimeEntity = datetime_ns.class_("DateTimeEntity", DateTimeBase)
 
 # Actions
 DateSetAction = datetime_ns.class_("DateSetAction", automation.Action)
 TimeSetAction = datetime_ns.class_("TimeSetAction", automation.Action)
+DateTimeSetAction = datetime_ns.class_("DateTimeSetAction", automation.Action)
 
 DateTimeStateTrigger = datetime_ns.class_(
     "DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime)
@@ -46,6 +49,12 @@ DateTimeStateTrigger = datetime_ns.class_(
 OnTimeTrigger = datetime_ns.class_(
     "OnTimeTrigger", automation.Trigger, cg.Component, cg.Parented.template(TimeEntity)
 )
+OnDateTimeTrigger = datetime_ns.class_(
+    "OnDateTimeTrigger",
+    automation.Trigger,
+    cg.Component,
+    cg.Parented.template(DateTimeEntity),
+)
 
 DATETIME_MODES = [
     "DATE",
@@ -61,45 +70,55 @@ _DATETIME_SCHEMA = cv.Schema(
                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger),
             }
         ),
+        cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
     }
 ).extend(cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA))
 
 
 def date_schema(class_: MockObjClass) -> cv.Schema:
-    schema = {
-        cv.GenerateID(): cv.declare_id(class_),
-        cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDateComponent),
-        cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True),
-    }
+    schema = cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(class_),
+            cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDateComponent),
+            cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True),
+        }
+    )
     return _DATETIME_SCHEMA.extend(schema)
 
 
 def time_schema(class_: MockObjClass) -> cv.Schema:
-    schema = {
-        cv.GenerateID(): cv.declare_id(class_),
-        cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTimeComponent),
-        cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True),
-        cv.Inclusive(
-            CONF_ON_TIME,
-            group_of_inclusion=CONF_ON_TIME,
-            msg="`on_time` and `time_id` must both be specified",
-        ): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTimeTrigger),
-            }
-        ),
-        cv.Inclusive(CONF_TIME_ID, group_of_inclusion=CONF_ON_TIME): cv.use_id(
-            time.RealTimeClock
-        ),
-    }
+    schema = cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(class_),
+            cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTimeComponent),
+            cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True),
+            cv.Optional(CONF_ON_TIME): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTimeTrigger),
+                }
+            ),
+        }
+    )
     return _DATETIME_SCHEMA.extend(schema)
 
 
 def datetime_schema(class_: MockObjClass) -> cv.Schema:
-    schema = {
-        cv.GenerateID(): cv.declare_id(class_),
-        cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of("DATETIME", upper=True),
-    }
+    schema = cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(class_),
+            cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
+                mqtt.MQTTDateTimeComponent
+            ),
+            cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of(
+                "DATETIME", upper=True
+            ),
+            cv.Optional(CONF_ON_TIME): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnDateTimeTrigger),
+                }
+            ),
+        }
+    )
     return _DATETIME_SCHEMA.extend(schema)
 
 
@@ -113,13 +132,11 @@ async def setup_datetime_core_(var, config):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
         await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf)
 
-    rtc_id = config.get(CONF_TIME_ID)
-    rtc = None
-    if rtc_id is not None:
-        rtc = await cg.get_variable(rtc_id)
+    rtc = await cg.get_variable(config[CONF_TIME_ID])
+    cg.add(var.set_rtc(rtc))
+
     for conf in config.get(CONF_ON_TIME, []):
-        assert rtc is not None
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], rtc)
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
         await automation.build_automation(trigger, [], conf)
         await cg.register_component(trigger, conf)
         await cg.register_parented(trigger, var)
@@ -161,16 +178,16 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args):
     action_var = cg.new_Pvariable(action_id, template_arg)
     await cg.register_parented(action_var, config[CONF_ID])
 
-    date = config[CONF_DATE]
-    if cg.is_template(date):
-        template_ = await cg.templatable(config[CONF_DATE], [], cg.ESPTime)
+    date_config = config[CONF_DATE]
+    if cg.is_template(date_config):
+        template_ = await cg.templatable(date_config, [], cg.ESPTime)
         cg.add(action_var.set_date(template_))
     else:
         date_struct = cg.StructInitializer(
             cg.ESPTime,
-            ("day_of_month", date[CONF_DAY]),
-            ("month", date[CONF_MONTH]),
-            ("year", date[CONF_YEAR]),
+            ("day_of_month", date_config[CONF_DAY]),
+            ("month", date_config[CONF_MONTH]),
+            ("year", date_config[CONF_YEAR]),
         )
         cg.add(action_var.set_date(date_struct))
     return action_var
@@ -194,7 +211,7 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args):
 
     time_config = config[CONF_TIME]
     if cg.is_template(time_config):
-        template_ = await cg.templatable(config[CONF_TIME], [], cg.ESPTime)
+        template_ = await cg.templatable(time_config, [], cg.ESPTime)
         cg.add(action_var.set_time(template_))
     else:
         time_struct = cg.StructInitializer(
@@ -205,3 +222,35 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args):
         )
         cg.add(action_var.set_time(time_struct))
     return action_var
+
+
+@automation.register_action(
+    "datetime.datetime.set",
+    DateTimeSetAction,
+    cv.Schema(
+        {
+            cv.Required(CONF_ID): cv.use_id(DateTimeEntity),
+            cv.Required(CONF_DATETIME): cv.Any(cv.returning_lambda, cv.date_time()),
+        },
+    ),
+)
+async def datetime_datetime_set_to_code(config, action_id, template_arg, args):
+    action_var = cg.new_Pvariable(action_id, template_arg)
+    await cg.register_parented(action_var, config[CONF_ID])
+
+    datetime_config = config[CONF_DATETIME]
+    if cg.is_template(datetime_config):
+        template_ = await cg.templatable(datetime_config, [], cg.ESPTime)
+        cg.add(action_var.set_datetime(template_))
+    else:
+        datetime_struct = cg.StructInitializer(
+            cg.ESPTime,
+            ("second", datetime_config[CONF_SECOND]),
+            ("minute", datetime_config[CONF_MINUTE]),
+            ("hour", datetime_config[CONF_HOUR]),
+            ("day_of_month", datetime_config[CONF_DAY]),
+            ("month", datetime_config[CONF_MONTH]),
+            ("year", datetime_config[CONF_YEAR]),
+        )
+        cg.add(action_var.set_datetime(datetime_struct))
+    return action_var
diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp
index 8b58a8faf7..19399c1e59 100644
--- a/esphome/components/datetime/date_entity.cpp
+++ b/esphome/components/datetime/date_entity.cpp
@@ -40,10 +40,13 @@ void DateCall::validate_() {
   if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) {
     ESP_LOGE(TAG, "Year must be between 1970 and 3000");
     this->year_.reset();
+    this->month_.reset();
+    this->day_.reset();
   }
   if (this->month_.has_value() && (this->month_ < 1 || this->month_ > 12)) {
     ESP_LOGE(TAG, "Month must be between 1 and 12");
     this->month_.reset();
+    this->day_.reset();
   }
   if (this->day_.has_value()) {
     uint16_t year = 0;
diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h
index 2f2d27e102..c8240390e3 100644
--- a/esphome/components/datetime/datetime_base.h
+++ b/esphome/components/datetime/datetime_base.h
@@ -5,6 +5,8 @@
 #include "esphome/core/entity_base.h"
 #include "esphome/core/time.h"
 
+#include "esphome/components/time/real_time_clock.h"
+
 namespace esphome {
 namespace datetime {
 
@@ -17,9 +19,14 @@ class DateTimeBase : public EntityBase {
 
   void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
 
+  void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; }
+  time::RealTimeClock *get_rtc() const { return this->rtc_; }
+
  protected:
   CallbackManager<void()> state_callback_;
 
+  time::RealTimeClock *rtc_;
+
   bool has_state_{false};
 };
 
diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp
new file mode 100644
index 0000000000..9a61d341e4
--- /dev/null
+++ b/esphome/components/datetime/datetime_entity.cpp
@@ -0,0 +1,252 @@
+#include "datetime_entity.h"
+
+#ifdef USE_DATETIME_DATETIME
+
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace datetime {
+
+static const char *const TAG = "datetime.datetime_entity";
+
+void DateTimeEntity::publish_state() {
+  if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) {
+    this->has_state_ = false;
+    return;
+  }
+  if (this->year_ < 1970 || this->year_ > 3000) {
+    this->has_state_ = false;
+    ESP_LOGE(TAG, "Year must be between 1970 and 3000");
+    return;
+  }
+  if (this->month_ < 1 || this->month_ > 12) {
+    this->has_state_ = false;
+    ESP_LOGE(TAG, "Month must be between 1 and 12");
+    return;
+  }
+  if (this->day_ > days_in_month(this->month_, this->year_)) {
+    this->has_state_ = false;
+    ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_);
+    return;
+  }
+  if (this->hour_ > 23) {
+    this->has_state_ = false;
+    ESP_LOGE(TAG, "Hour must be between 0 and 23");
+    return;
+  }
+  if (this->minute_ > 59) {
+    this->has_state_ = false;
+    ESP_LOGE(TAG, "Minute must be between 0 and 59");
+    return;
+  }
+  if (this->second_ > 59) {
+    this->has_state_ = false;
+    ESP_LOGE(TAG, "Second must be between 0 and 59");
+    return;
+  }
+  this->has_state_ = true;
+  ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_,
+           this->month_, this->day_, this->hour_, this->minute_, this->second_);
+  this->state_callback_.call();
+}
+
+DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }
+
+ESPTime DateTimeEntity::state_as_esptime() const {
+  ESPTime obj;
+  obj.year = this->year_;
+  obj.month = this->month_;
+  obj.day_of_month = this->day_;
+  obj.hour = this->hour_;
+  obj.minute = this->minute_;
+  obj.second = this->second_;
+  obj.day_of_week = 1;  // Required to be valid for recalc_timestamp_local but not used.
+  obj.day_of_year = 1;  // Required to be valid for recalc_timestamp_local but not used.
+  obj.recalc_timestamp_local(false);
+  return obj;
+}
+
+void DateTimeCall::validate_() {
+  if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) {
+    ESP_LOGE(TAG, "Year must be between 1970 and 3000");
+    this->year_.reset();
+    this->month_.reset();
+    this->day_.reset();
+  }
+  if (this->month_.has_value() && (this->month_ < 1 || this->month_ > 12)) {
+    ESP_LOGE(TAG, "Month must be between 1 and 12");
+    this->month_.reset();
+    this->day_.reset();
+  }
+  if (this->day_.has_value()) {
+    uint16_t year = 0;
+    uint8_t month = 0;
+    if (this->month_.has_value()) {
+      month = *this->month_;
+    } else {
+      if (this->parent_->month != 0) {
+        month = this->parent_->month;
+      } else {
+        ESP_LOGE(TAG, "Month must be set to validate day");
+        this->day_.reset();
+      }
+    }
+    if (this->year_.has_value()) {
+      year = *this->year_;
+    } else {
+      if (this->parent_->year != 0) {
+        year = this->parent_->year;
+      } else {
+        ESP_LOGE(TAG, "Year must be set to validate day");
+        this->day_.reset();
+      }
+    }
+    if (this->day_.has_value() && *this->day_ > days_in_month(month, year)) {
+      ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(month, year), month);
+      this->day_.reset();
+    }
+  }
+
+  if (this->hour_.has_value() && this->hour_ > 23) {
+    ESP_LOGE(TAG, "Hour must be between 0 and 23");
+    this->hour_.reset();
+  }
+  if (this->minute_.has_value() && this->minute_ > 59) {
+    ESP_LOGE(TAG, "Minute must be between 0 and 59");
+    this->minute_.reset();
+  }
+  if (this->second_.has_value() && this->second_ > 59) {
+    ESP_LOGE(TAG, "Second must be between 0 and 59");
+    this->second_.reset();
+  }
+}
+
+void DateTimeCall::perform() {
+  this->validate_();
+  ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
+
+  if (this->year_.has_value()) {
+    ESP_LOGD(TAG, " Year: %d", *this->year_);
+  }
+  if (this->month_.has_value()) {
+    ESP_LOGD(TAG, " Month: %d", *this->month_);
+  }
+  if (this->day_.has_value()) {
+    ESP_LOGD(TAG, " Day: %d", *this->day_);
+  }
+  if (this->hour_.has_value()) {
+    ESP_LOGD(TAG, " Hour: %d", *this->hour_);
+  }
+  if (this->minute_.has_value()) {
+    ESP_LOGD(TAG, " Minute: %d", *this->minute_);
+  }
+  if (this->second_.has_value()) {
+    ESP_LOGD(TAG, " Second: %d", *this->second_);
+  }
+  this->parent_->control(*this);
+}
+
+DateTimeCall &DateTimeCall::set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
+                                         uint8_t second) {
+  this->year_ = year;
+  this->month_ = month;
+  this->day_ = day;
+  this->hour_ = hour;
+  this->minute_ = minute;
+  this->second_ = second;
+  return *this;
+};
+
+DateTimeCall &DateTimeCall::set_datetime(ESPTime datetime) {
+  return this->set_datetime(datetime.year, datetime.month, datetime.day_of_month, datetime.hour, datetime.minute,
+                            datetime.second);
+};
+
+DateTimeCall &DateTimeCall::set_datetime(const std::string &datetime) {
+  ESPTime val{};
+  if (!ESPTime::strptime(datetime, val)) {
+    ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
+    return *this;
+  }
+  return this->set_datetime(val);
+}
+
+DateTimeCall &DateTimeCall::set_datetime(time_t epoch_seconds) {
+  ESPTime val = ESPTime::from_epoch_local(epoch_seconds);
+  return this->set_datetime(val);
+}
+
+DateTimeCall DateTimeEntityRestoreState::to_call(DateTimeEntity *datetime) {
+  DateTimeCall call = datetime->make_call();
+  call.set_datetime(this->year, this->month, this->day, this->hour, this->minute, this->second);
+  return call;
+}
+
+void DateTimeEntityRestoreState::apply(DateTimeEntity *time) {
+  time->year_ = this->year;
+  time->month_ = this->month;
+  time->day_ = this->day;
+  time->hour_ = this->hour;
+  time->minute_ = this->minute;
+  time->second_ = this->second;
+  time->publish_state();
+}
+
+static const int MAX_TIMESTAMP_DRIFT = 900;  // how far can the clock drift before we consider
+                                             // there has been a drastic time synchronization
+
+void OnDateTimeTrigger::loop() {
+  if (!this->parent_->has_state()) {
+    return;
+  }
+  ESPTime time = this->parent_->rtc_->now();
+  if (!time.is_valid()) {
+    return;
+  }
+  if (this->last_check_.has_value()) {
+    if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) {
+      // We went back in time (a lot), probably caused by time synchronization
+      ESP_LOGW(TAG, "Time has jumped back!");
+    } else if (*this->last_check_ >= time) {
+      // already handled this one
+      return;
+    } else if (time > *this->last_check_ && time.timestamp - this->last_check_->timestamp > MAX_TIMESTAMP_DRIFT) {
+      // We went ahead in time (a lot), probably caused by time synchronization
+      ESP_LOGW(TAG, "Time has jumped ahead!");
+      this->last_check_ = time;
+      return;
+    }
+
+    while (true) {
+      this->last_check_->increment_second();
+      if (*this->last_check_ >= time)
+        break;
+
+      if (this->matches_(*this->last_check_)) {
+        this->trigger();
+        break;
+      }
+    }
+  }
+
+  this->last_check_ = time;
+  if (!time.fields_in_range()) {
+    ESP_LOGW(TAG, "Time is out of range!");
+    ESP_LOGD(TAG, "Second=%02u Minute=%02u Hour=%02u Day=%02u Month=%02u Year=%04u", time.second, time.minute,
+             time.hour, time.day_of_month, time.month, time.year);
+  }
+
+  if (this->matches_(time))
+    this->trigger();
+}
+
+bool OnDateTimeTrigger::matches_(const ESPTime &time) const {
+  return time.is_valid() && time.year == this->parent_->year && time.month == this->parent_->month &&
+         time.day_of_month == this->parent_->day && time.hour == this->parent_->hour &&
+         time.minute == this->parent_->minute && time.second == this->parent_->second;
+}
+
+}  // namespace datetime
+}  // namespace esphome
+
+#endif  // USE_DATETIME_TIME
diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h
new file mode 100644
index 0000000000..d541fa96b1
--- /dev/null
+++ b/esphome/components/datetime/datetime_entity.h
@@ -0,0 +1,150 @@
+#pragma once
+
+#include "esphome/core/defines.h"
+
+#ifdef USE_DATETIME_DATETIME
+
+#include "esphome/core/automation.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/time.h"
+
+#include "datetime_base.h"
+
+namespace esphome {
+namespace datetime {
+
+#define LOG_DATETIME_DATETIME(prefix, type, obj) \
+  if ((obj) != nullptr) { \
+    ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
+    if (!(obj)->get_icon().empty()) { \
+      ESP_LOGCONFIG(TAG, "%s  Icon: '%s'", prefix, (obj)->get_icon().c_str()); \
+    } \
+  }
+
+class DateTimeCall;
+class DateTimeEntity;
+
+struct DateTimeEntityRestoreState {
+  uint16_t year;
+  uint8_t month;
+  uint8_t day;
+  uint8_t hour;
+  uint8_t minute;
+  uint8_t second;
+
+  DateTimeCall to_call(DateTimeEntity *datetime);
+  void apply(DateTimeEntity *datetime);
+} __attribute__((packed));
+
+class DateTimeEntity : public DateTimeBase {
+ protected:
+  uint16_t year_;
+  uint8_t month_;
+  uint8_t day_;
+  uint8_t hour_;
+  uint8_t minute_;
+  uint8_t second_;
+
+ public:
+  void publish_state();
+  DateTimeCall make_call();
+
+  ESPTime state_as_esptime() const override;
+
+  const uint16_t &year = year_;
+  const uint8_t &month = month_;
+  const uint8_t &day = day_;
+  const uint8_t &hour = hour_;
+  const uint8_t &minute = minute_;
+  const uint8_t &second = second_;
+
+ protected:
+  friend class DateTimeCall;
+  friend struct DateTimeEntityRestoreState;
+  friend class OnDateTimeTrigger;
+
+  virtual void control(const DateTimeCall &call) = 0;
+};
+
+class DateTimeCall {
+ public:
+  explicit DateTimeCall(DateTimeEntity *parent) : parent_(parent) {}
+  void perform();
+  DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
+  DateTimeCall &set_datetime(ESPTime datetime);
+  DateTimeCall &set_datetime(const std::string &datetime);
+  DateTimeCall &set_datetime(time_t epoch_seconds);
+
+  DateTimeCall &set_year(uint16_t year) {
+    this->year_ = year;
+    return *this;
+  }
+  DateTimeCall &set_month(uint8_t month) {
+    this->month_ = month;
+    return *this;
+  }
+  DateTimeCall &set_day(uint8_t day) {
+    this->day_ = day;
+    return *this;
+  }
+  DateTimeCall &set_hour(uint8_t hour) {
+    this->hour_ = hour;
+    return *this;
+  }
+  DateTimeCall &set_minute(uint8_t minute) {
+    this->minute_ = minute;
+    return *this;
+  }
+  DateTimeCall &set_second(uint8_t second) {
+    this->second_ = second;
+    return *this;
+  }
+
+  optional<uint16_t> get_year() const { return this->year_; }
+  optional<uint8_t> get_month() const { return this->month_; }
+  optional<uint8_t> get_day() const { return this->day_; }
+  optional<uint8_t> get_hour() const { return this->hour_; }
+  optional<uint8_t> get_minute() const { return this->minute_; }
+  optional<uint8_t> get_second() const { return this->second_; }
+
+ protected:
+  void validate_();
+
+  DateTimeEntity *parent_;
+
+  optional<uint16_t> year_;
+  optional<uint8_t> month_;
+  optional<uint8_t> day_;
+  optional<uint8_t> hour_;
+  optional<uint8_t> minute_;
+  optional<uint8_t> second_;
+};
+
+template<typename... Ts> class DateTimeSetAction : public Action<Ts...>, public Parented<DateTimeEntity> {
+ public:
+  TEMPLATABLE_VALUE(ESPTime, datetime)
+
+  void play(Ts... x) override {
+    auto call = this->parent_->make_call();
+
+    if (this->datetime_.has_value()) {
+      call.set_datetime(this->datetime_.value(x...));
+    }
+    call.perform();
+  }
+};
+
+class OnDateTimeTrigger : public Trigger<>, public Component, public Parented<DateTimeEntity> {
+ public:
+  void loop() override;
+
+ protected:
+  bool matches_(const ESPTime &time) const;
+
+  optional<ESPTime> last_check_;
+};
+
+}  // namespace datetime
+}  // namespace esphome
+
+#endif  // USE_DATETIME_DATETIME
diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp
index 98558152d7..ea5e6684d0 100644
--- a/esphome/components/datetime/time_entity.cpp
+++ b/esphome/components/datetime/time_entity.cpp
@@ -94,8 +94,6 @@ void TimeEntityRestoreState::apply(TimeEntity *time) {
   time->publish_state();
 }
 
-#ifdef USE_TIME
-
 static const int MAX_TIMESTAMP_DRIFT = 900;  // how far can the clock drift before we consider
                                              // there has been a drastic time synchronization
 
@@ -103,7 +101,7 @@ void OnTimeTrigger::loop() {
   if (!this->parent_->has_state()) {
     return;
   }
-  ESPTime time = this->rtc_->now();
+  ESPTime time = this->parent_->rtc_->now();
   if (!time.is_valid()) {
     return;
   }
@@ -148,8 +146,6 @@ bool OnTimeTrigger::matches_(const ESPTime &time) const {
          time.second == this->parent_->second;
 }
 
-#endif
-
 }  // namespace datetime
 }  // namespace esphome
 
diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h
index 956c09e2b4..62e593d28a 100644
--- a/esphome/components/datetime/time_entity.h
+++ b/esphome/components/datetime/time_entity.h
@@ -10,10 +10,6 @@
 
 #include "datetime_base.h"
 
-#ifdef USE_TIME
-#include "esphome/components/time/real_time_clock.h"
-#endif
-
 namespace esphome {
 namespace datetime {
 
@@ -27,6 +23,7 @@ namespace datetime {
 
 class TimeCall;
 class TimeEntity;
+class OnTimeTrigger;
 
 struct TimeEntityRestoreState {
   uint8_t hour;
@@ -62,6 +59,7 @@ class TimeEntity : public DateTimeBase {
  protected:
   friend class TimeCall;
   friend struct TimeEntityRestoreState;
+  friend class OnTimeTrigger;
 
   virtual void control(const TimeCall &call) = 0;
 };
@@ -115,22 +113,16 @@ template<typename... Ts> class TimeSetAction : public Action<Ts...>, public Pare
   }
 };
 
-#ifdef USE_TIME
-
 class OnTimeTrigger : public Trigger<>, public Component, public Parented<TimeEntity> {
  public:
-  explicit OnTimeTrigger(time::RealTimeClock *rtc) : rtc_(rtc) {}
   void loop() override;
 
  protected:
   bool matches_(const ESPTime &time) const;
 
-  time::RealTimeClock *rtc_;
   optional<ESPTime> last_check_;
 };
 
-#endif
-
 }  // namespace datetime
 }  // namespace esphome
 
diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py
index 7a42140ef6..064362c619 100644
--- a/esphome/components/mqtt/__init__.py
+++ b/esphome/components/mqtt/__init__.py
@@ -115,6 +115,7 @@ MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent)
 MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent)
 MQTTDateComponent = mqtt_ns.class_("MQTTDateComponent", MQTTComponent)
 MQTTTimeComponent = mqtt_ns.class_("MQTTTimeComponent", MQTTComponent)
+MQTTDateTimeComponent = mqtt_ns.class_("MQTTDateTimeComponent", MQTTComponent)
 MQTTTextComponent = mqtt_ns.class_("MQTTTextComponent", MQTTComponent)
 MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent)
 MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent)
diff --git a/esphome/components/mqtt/mqtt_datetime.cpp b/esphome/components/mqtt/mqtt_datetime.cpp
new file mode 100644
index 0000000000..4fa44aafb8
--- /dev/null
+++ b/esphome/components/mqtt/mqtt_datetime.cpp
@@ -0,0 +1,84 @@
+#include "mqtt_datetime.h"
+
+#include <utility>
+#include "esphome/core/log.h"
+
+#include "mqtt_const.h"
+
+#ifdef USE_MQTT
+#ifdef USE_DATETIME_TIME
+
+namespace esphome {
+namespace mqtt {
+
+static const char *const TAG = "mqtt.datetime.time";
+
+using namespace esphome::datetime;
+
+MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetime_(datetime) {}
+
+void MQTTDateTimeComponent::setup() {
+  this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
+    auto call = this->datetime_->make_call();
+    if (root.containsKey("year")) {
+      call.set_year(root["year"]);
+    }
+    if (root.containsKey("month")) {
+      call.set_month(root["month"]);
+    }
+    if (root.containsKey("day")) {
+      call.set_day(root["day"]);
+    }
+    if (root.containsKey("hour")) {
+      call.set_hour(root["hour"]);
+    }
+    if (root.containsKey("minute")) {
+      call.set_minute(root["minute"]);
+    }
+    if (root.containsKey("second")) {
+      call.set_second(root["second"]);
+    }
+    call.perform();
+  });
+  this->datetime_->add_on_state_callback([this]() {
+    this->publish_state(this->datetime_->year, this->datetime_->month, this->datetime_->day, this->datetime_->hour,
+                        this->datetime_->minute, this->datetime_->second);
+  });
+}
+
+void MQTTDateTimeComponent::dump_config() {
+  ESP_LOGCONFIG(TAG, "MQTT DateTime '%s':", this->datetime_->get_name().c_str());
+  LOG_MQTT_COMPONENT(true, true)
+}
+
+std::string MQTTDateTimeComponent::component_type() const { return "datetime"; }
+const EntityBase *MQTTDateTimeComponent::get_entity() const { return this->datetime_; }
+
+void MQTTDateTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
+  // Nothing extra to add here
+}
+bool MQTTDateTimeComponent::send_initial_state() {
+  if (this->datetime_->has_state()) {
+    return this->publish_state(this->datetime_->year, this->datetime_->month, this->datetime_->day,
+                               this->datetime_->hour, this->datetime_->minute, this->datetime_->second);
+  } else {
+    return true;
+  }
+}
+bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
+                                          uint8_t second) {
+  return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
+    root["year"] = year;
+    root["month"] = month;
+    root["day"] = day;
+    root["hour"] = hour;
+    root["minute"] = minute;
+    root["second"] = second;
+  });
+}
+
+}  // namespace mqtt
+}  // namespace esphome
+
+#endif  // USE_DATETIME_TIME
+#endif  // USE_MQTT
diff --git a/esphome/components/mqtt/mqtt_datetime.h b/esphome/components/mqtt/mqtt_datetime.h
new file mode 100644
index 0000000000..f0d68ad2e1
--- /dev/null
+++ b/esphome/components/mqtt/mqtt_datetime.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "esphome/core/defines.h"
+
+#ifdef USE_MQTT
+#ifdef USE_DATETIME_TIME
+
+#include "esphome/components/datetime/datetime_entity.h"
+#include "mqtt_component.h"
+
+namespace esphome {
+namespace mqtt {
+
+class MQTTDateTimeComponent : public mqtt::MQTTComponent {
+ public:
+  /** Construct this MQTTDateTimeComponent instance with the provided friendly_name and time
+   *
+   * @param time The time entity.
+   */
+  explicit MQTTDateTimeComponent(datetime::DateTimeEntity *time);
+
+  // ========== INTERNAL METHODS ==========
+  // (In most use cases you won't need these)
+  /// Override setup.
+  void setup() override;
+  void dump_config() override;
+
+  void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override;
+
+  bool send_initial_state() override;
+
+  bool publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
+
+ protected:
+  std::string component_type() const override;
+  const EntityBase *get_entity() const override;
+
+  datetime::DateTimeEntity *datetime_;
+};
+
+}  // namespace mqtt
+}  // namespace esphome
+
+#endif  // USE_DATETIME_DATE
+#endif  // USE_MQTT
diff --git a/esphome/components/template/datetime/__init__.py b/esphome/components/template/datetime/__init__.py
index 53d9d1b9d3..bf7154ef76 100644
--- a/esphome/components/template/datetime/__init__.py
+++ b/esphome/components/template/datetime/__init__.py
@@ -31,6 +31,10 @@ TemplateTime = template_ns.class_(
     "TemplateTime", datetime.TimeEntity, cg.PollingComponent
 )
 
+TemplateDateTime = template_ns.class_(
+    "TemplateDateTime", datetime.DateTimeEntity, cg.PollingComponent
+)
+
 
 def validate(config):
     config = config.copy()
@@ -78,6 +82,13 @@ CONFIG_SCHEMA = cv.All(
                     cv.Optional(CONF_INITIAL_VALUE): cv.date_time(allowed_date=False),
                 }
             ),
+            "DATETIME": datetime.datetime_schema(TemplateDateTime)
+            .extend(_BASE_SCHEMA)
+            .extend(
+                {
+                    cv.Optional(CONF_INITIAL_VALUE): cv.date_time(),
+                }
+            ),
         },
         upper=True,
     ),
@@ -116,6 +127,17 @@ async def to_code(config):
                     ("hour", initial_value[CONF_HOUR]),
                 )
                 cg.add(var.set_initial_value(time_struct))
+            elif config[CONF_TYPE] == "DATETIME":
+                datetime_struct = cg.StructInitializer(
+                    cg.ESPTime,
+                    ("second", initial_value[CONF_SECOND]),
+                    ("minute", initial_value[CONF_MINUTE]),
+                    ("hour", initial_value[CONF_HOUR]),
+                    ("day_of_month", initial_value[CONF_DAY]),
+                    ("month", initial_value[CONF_MONTH]),
+                    ("year", initial_value[CONF_YEAR]),
+                )
+                cg.add(var.set_initial_value(datetime_struct))
 
     if CONF_SET_ACTION in config:
         await automation.build_automation(
diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp
new file mode 100644
index 0000000000..3ab74e197f
--- /dev/null
+++ b/esphome/components/template/datetime/template_datetime.cpp
@@ -0,0 +1,150 @@
+#include "template_datetime.h"
+
+#ifdef USE_DATETIME_DATETIME
+
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace template_ {
+
+static const char *const TAG = "template.datetime";
+
+void TemplateDateTime::setup() {
+  if (this->f_.has_value())
+    return;
+
+  ESPTime state{};
+
+  if (!this->restore_value_) {
+    state = this->initial_value_;
+  } else {
+    datetime::DateTimeEntityRestoreState temp;
+    this->pref_ = global_preferences->make_preference<datetime::DateTimeEntityRestoreState>(194434090U ^
+                                                                                            this->get_object_id_hash());
+    if (this->pref_.load(&temp)) {
+      temp.apply(this);
+      return;
+    } else {
+      // set to inital value if loading from pref failed
+      state = this->initial_value_;
+    }
+  }
+
+  this->year_ = state.year;
+  this->month_ = state.month;
+  this->day_ = state.day_of_month;
+  this->hour_ = state.hour;
+  this->minute_ = state.minute;
+  this->second_ = state.second;
+  this->publish_state();
+}
+
+void TemplateDateTime::update() {
+  if (!this->f_.has_value())
+    return;
+
+  auto val = (*this->f_)();
+  if (!val.has_value())
+    return;
+
+  this->year_ = val->year;
+  this->month_ = val->month;
+  this->day_ = val->day_of_month;
+  this->hour_ = val->hour;
+  this->minute_ = val->minute;
+  this->second_ = val->second;
+  this->publish_state();
+}
+
+void TemplateDateTime::control(const datetime::DateTimeCall &call) {
+  bool has_year = call.get_year().has_value();
+  bool has_month = call.get_month().has_value();
+  bool has_day = call.get_day().has_value();
+  bool has_hour = call.get_hour().has_value();
+  bool has_minute = call.get_minute().has_value();
+  bool has_second = call.get_second().has_value();
+
+  ESPTime value = {};
+  if (has_year)
+    value.year = *call.get_year();
+
+  if (has_month)
+    value.month = *call.get_month();
+
+  if (has_day)
+    value.day_of_month = *call.get_day();
+
+  if (has_hour)
+    value.hour = *call.get_hour();
+
+  if (has_minute)
+    value.minute = *call.get_minute();
+
+  if (has_second)
+    value.second = *call.get_second();
+
+  this->set_trigger_->trigger(value);
+
+  if (this->optimistic_) {
+    if (has_year)
+      this->year_ = *call.get_year();
+    if (has_month)
+      this->month_ = *call.get_month();
+    if (has_day)
+      this->day_ = *call.get_day();
+    if (has_hour)
+      this->hour_ = *call.get_hour();
+    if (has_minute)
+      this->minute_ = *call.get_minute();
+    if (has_second)
+      this->second_ = *call.get_second();
+    this->publish_state();
+  }
+
+  if (this->restore_value_) {
+    datetime::DateTimeEntityRestoreState temp = {};
+    if (has_year) {
+      temp.year = *call.get_year();
+    } else {
+      temp.year = this->year_;
+    }
+    if (has_month) {
+      temp.month = *call.get_month();
+    } else {
+      temp.month = this->month_;
+    }
+    if (has_day) {
+      temp.day = *call.get_day();
+    } else {
+      temp.day = this->day_;
+    }
+    if (has_hour) {
+      temp.hour = *call.get_hour();
+    } else {
+      temp.hour = this->hour_;
+    }
+    if (has_minute) {
+      temp.minute = *call.get_minute();
+    } else {
+      temp.minute = this->minute_;
+    }
+    if (has_second) {
+      temp.second = *call.get_second();
+    } else {
+      temp.second = this->second_;
+    }
+
+    this->pref_.save(&temp);
+  }
+}
+
+void TemplateDateTime::dump_config() {
+  LOG_DATETIME_DATETIME("", "Template DateTime", this);
+  ESP_LOGCONFIG(TAG, "  Optimistic: %s", YESNO(this->optimistic_));
+  LOG_UPDATE_INTERVAL(this);
+}
+
+}  // namespace template_
+}  // namespace esphome
+
+#endif  // USE_DATETIME_DATETIME
diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h
new file mode 100644
index 0000000000..cb1fd01132
--- /dev/null
+++ b/esphome/components/template/datetime/template_datetime.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "esphome/core/defines.h"
+
+#ifdef USE_DATETIME_TIME
+
+#include "esphome/components/datetime/datetime_entity.h"
+#include "esphome/core/automation.h"
+#include "esphome/core/component.h"
+#include "esphome/core/preferences.h"
+#include "esphome/core/time.h"
+
+namespace esphome {
+namespace template_ {
+
+class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent {
+ public:
+  void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; }
+
+  void setup() override;
+  void update() override;
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+
+  Trigger<ESPTime> *get_set_trigger() const { return this->set_trigger_; }
+  void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
+
+  void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; }
+  void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
+
+ protected:
+  void control(const datetime::DateTimeCall &call) override;
+
+  bool optimistic_{false};
+  ESPTime initial_value_{};
+  bool restore_value_{false};
+  Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
+  optional<std::function<optional<ESPTime>()>> f_;
+
+  ESPPreferenceObject pref_;
+};
+
+}  // namespace template_
+}  // namespace esphome
+
+#endif  // USE_DATETIME_TIME
diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp
index 9b903d098b..2b9a95c6bd 100644
--- a/esphome/components/time/real_time_clock.cpp
+++ b/esphome/components/time/real_time_clock.cpp
@@ -13,6 +13,8 @@
 #endif
 #include <cerrno>
 
+#include <cinttypes>
+
 namespace esphome {
 namespace time {
 
diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp
index 8d08783c8c..42af72e872 100644
--- a/esphome/components/web_server/list_entities.cpp
+++ b/esphome/components/web_server/list_entities.cpp
@@ -129,6 +129,15 @@ bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) {
 }
 #endif
 
+#ifdef USE_DATETIME_DATETIME
+bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) {
+  if (this->web_server_->events_.count() == 0)
+    return true;
+  this->web_server_->events_.send(this->web_server_->datetime_json(datetime, DETAIL_ALL).c_str(), "state");
+  return true;
+}
+#endif
+
 #ifdef USE_TEXT
 bool ListEntitiesIterator::on_text(text::Text *text) {
   if (this->web_server_->events_.count() == 0)
diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h
index af84cb1d2b..47d427d9b5 100644
--- a/esphome/components/web_server/list_entities.h
+++ b/esphome/components/web_server/list_entities.h
@@ -47,6 +47,9 @@ class ListEntitiesIterator : public ComponentIterator {
 #ifdef USE_DATETIME_TIME
   bool on_time(datetime::TimeEntity *time) override;
 #endif
+#ifdef USE_DATETIME_DATETIME
+  bool on_datetime(datetime::DateTimeEntity *datetime) override;
+#endif
 #ifdef USE_TEXT
   bool on_text(text::Text *text) override;
 #endif
diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index 0202038ffc..6a7b4121f0 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -926,6 +926,8 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con
 
 #ifdef USE_DATETIME_TIME
 void WebServer::on_time_update(datetime::TimeEntity *obj) {
+  if (this->events_.count() == 0)
+    return;
   this->events_.send(this->time_json(obj, DETAIL_STATE).c_str(), "state");
 }
 void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -970,6 +972,55 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con
 }
 #endif  // USE_DATETIME_TIME
 
+#ifdef USE_DATETIME_DATETIME
+void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) {
+  if (this->events_.count() == 0)
+    return;
+  this->events_.send(this->datetime_json(obj, DETAIL_STATE).c_str(), "state");
+}
+void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) {
+  for (auto *obj : App.get_datetimes()) {
+    if (obj->get_object_id() != match.id)
+      continue;
+    if (request->method() == HTTP_GET && match.method.empty()) {
+      std::string data = this->datetime_json(obj, DETAIL_STATE);
+      request->send(200, "application/json", data.c_str());
+      return;
+    }
+    if (match.method != "set") {
+      request->send(404);
+      return;
+    }
+
+    auto call = obj->make_call();
+
+    if (!request->hasParam("value")) {
+      request->send(409);
+      return;
+    }
+
+    if (request->hasParam("value")) {
+      std::string value = request->getParam("value")->value().c_str();
+      call.set_datetime(value);
+    }
+
+    this->schedule_([call]() mutable { call.perform(); });
+    request->send(200);
+    return;
+  }
+  request->send(404);
+}
+std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) {
+  return json::build_json([obj, start_config](JsonObject root) {
+    set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config);
+    std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour,
+                                    obj->minute, obj->second);
+    root["value"] = value;
+    root["state"] = value;
+  });
+}
+#endif  // USE_DATETIME_DATETIME
+
 #ifdef USE_TEXT
 void WebServer::on_text_update(text::Text *obj, const std::string &state) {
   if (this->events_.count() == 0)
@@ -1458,6 +1509,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
     return true;
 #endif
 
+#ifdef USE_DATETIME_DATETIME
+  if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "datetime")
+    return true;
+#endif
+
 #ifdef USE_TEXT
   if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "text")
     return true;
@@ -1595,6 +1651,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
   }
 #endif
 
+#ifdef USE_DATETIME_DATETIME
+  if (match.domain == "datetime") {
+    this->handle_datetime_request(request, match);
+    return;
+  }
+#endif
+
 #ifdef USE_TEXT
   if (match.domain == "text") {
     this->handle_text_request(request, match);
diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h
index 5e8f3f8236..dda14a7e05 100644
--- a/esphome/components/web_server/web_server.h
+++ b/esphome/components/web_server/web_server.h
@@ -239,6 +239,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   std::string time_json(datetime::TimeEntity *obj, JsonDetail start_config);
 #endif
 
+#ifdef USE_DATETIME_DATETIME
+  void on_datetime_update(datetime::DateTimeEntity *obj) override;
+  /// Handle a datetime request under '/datetime/<id>'.
+  void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match);
+
+  /// Dump the datetime state with its value as a JSON string.
+  std::string datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config);
+#endif
+
 #ifdef USE_TEXT
   void on_text_update(text::Text *obj, const std::string &state) override;
   /// Handle a text input request under '/text/<id>'.
diff --git a/esphome/const.py b/esphome/const.py
index a64bc73f59..324b32e847 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -179,6 +179,7 @@ CONF_DATA_PINS = "data_pins"
 CONF_DATA_RATE = "data_rate"
 CONF_DATA_TEMPLATE = "data_template"
 CONF_DATE = "date"
+CONF_DATETIME = "datetime"
 CONF_DAY = "day"
 CONF_DAYS_OF_MONTH = "days_of_month"
 CONF_DAYS_OF_WEEK = "days_of_week"
diff --git a/esphome/core/application.h b/esphome/core/application.h
index 35df350ec3..7487780412 100644
--- a/esphome/core/application.h
+++ b/esphome/core/application.h
@@ -45,6 +45,9 @@
 #ifdef USE_DATETIME_TIME
 #include "esphome/components/datetime/time_entity.h"
 #endif
+#ifdef USE_DATETIME_DATETIME
+#include "esphome/components/datetime/datetime_entity.h"
+#endif
 #ifdef USE_TEXT
 #include "esphome/components/text/text.h"
 #endif
@@ -141,6 +144,10 @@ class Application {
   void register_time(datetime::TimeEntity *time) { this->times_.push_back(time); }
 #endif
 
+#ifdef USE_DATETIME_DATETIME
+  void register_datetime(datetime::DateTimeEntity *datetime) { this->datetimes_.push_back(datetime); }
+#endif
+
 #ifdef USE_TEXT
   void register_text(text::Text *text) { this->texts_.push_back(text); }
 #endif
@@ -335,6 +342,15 @@ class Application {
     return nullptr;
   }
 #endif
+#ifdef USE_DATETIME_DATETIME
+  const std::vector<datetime::DateTimeEntity *> &get_datetimes() { return this->datetimes_; }
+  datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) {
+    for (auto *obj : this->datetimes_)
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
+        return obj;
+    return nullptr;
+  }
+#endif
 #ifdef USE_TEXT
   const std::vector<text::Text *> &get_texts() { return this->texts_; }
   text::Text *get_text_by_key(uint32_t key, bool include_internal = false) {
@@ -456,6 +472,9 @@ class Application {
 #ifdef USE_DATETIME_TIME
   std::vector<datetime::TimeEntity *> times_{};
 #endif
+#ifdef USE_DATETIME_DATETIME
+  std::vector<datetime::DateTimeEntity *> datetimes_{};
+#endif
 #ifdef USE_SELECT
   std::vector<select::Select *> selects_{};
 #endif
diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp
index 687f1f6e23..9b02bf527b 100644
--- a/esphome/core/component_iterator.cpp
+++ b/esphome/core/component_iterator.cpp
@@ -232,6 +232,21 @@ void ComponentIterator::advance() {
       }
       break;
 #endif
+#ifdef USE_DATETIME_DATETIME
+    case IteratorState::DATETIME_DATETIME:
+      if (this->at_ >= App.get_datetimes().size()) {
+        advance_platform = true;
+      } else {
+        auto *datetime = App.get_datetimes()[this->at_];
+        if (datetime->is_internal() && !this->include_internal_) {
+          success = true;
+          break;
+        } else {
+          success = this->on_datetime(datetime);
+        }
+      }
+      break;
+#endif
 #ifdef USE_TEXT
     case IteratorState::TEXT:
       if (this->at_ >= App.get_texts().size()) {
diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h
index 8f0398cbb3..2b847bc088 100644
--- a/esphome/core/component_iterator.h
+++ b/esphome/core/component_iterator.h
@@ -63,6 +63,9 @@ class ComponentIterator {
 #ifdef USE_DATETIME_TIME
   virtual bool on_time(datetime::TimeEntity *time) = 0;
 #endif
+#ifdef USE_DATETIME_DATETIME
+  virtual bool on_datetime(datetime::DateTimeEntity *datetime) = 0;
+#endif
 #ifdef USE_TEXT
   virtual bool on_text(text::Text *text) = 0;
 #endif
@@ -132,6 +135,9 @@ class ComponentIterator {
 #ifdef USE_DATETIME_TIME
     DATETIME_TIME,
 #endif
+#ifdef USE_DATETIME_DATETIME
+    DATETIME_DATETIME,
+#endif
 #ifdef USE_TEXT
     TEXT,
 #endif
diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp
index eb975eaf6f..0957329500 100644
--- a/esphome/core/controller.cpp
+++ b/esphome/core/controller.cpp
@@ -71,6 +71,12 @@ void Controller::setup_controller(bool include_internal) {
       obj->add_on_state_callback([this, obj]() { this->on_time_update(obj); });
   }
 #endif
+#ifdef USE_DATETIME_DATETIME
+  for (auto *obj : App.get_datetimes()) {
+    if (include_internal || !obj->is_internal())
+      obj->add_on_state_callback([this, obj]() { this->on_datetime_update(obj); });
+  }
+#endif
 #ifdef USE_TEXT
   for (auto *obj : App.get_texts()) {
     if (include_internal || !obj->is_internal())
diff --git a/esphome/core/controller.h b/esphome/core/controller.h
index da9dbc00a6..e1bf93193a 100644
--- a/esphome/core/controller.h
+++ b/esphome/core/controller.h
@@ -37,6 +37,9 @@
 #ifdef USE_DATETIME_TIME
 #include "esphome/components/datetime/time_entity.h"
 #endif
+#ifdef USE_DATETIME_DATETIME
+#include "esphome/components/datetime/datetime_entity.h"
+#endif
 #ifdef USE_TEXT
 #include "esphome/components/text/text.h"
 #endif
@@ -97,6 +100,9 @@ class Controller {
 #ifdef USE_DATETIME_TIME
   virtual void on_time_update(datetime::TimeEntity *obj){};
 #endif
+#ifdef USE_DATETIME_DATETIME
+  virtual void on_datetime_update(datetime::DateTimeEntity *obj){};
+#endif
 #ifdef USE_TEXT
   virtual void on_text_update(text::Text *obj, const std::string &state){};
 #endif
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index fed73098d2..619a956071 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -39,6 +39,7 @@
 #define USE_DATETIME
 #define USE_DATETIME_DATE
 #define USE_DATETIME_TIME
+#define USE_DATETIME_DATETIME
 #define USE_OTA
 #define USE_OTA_PASSWORD
 #define USE_OTA_STATE_CALLBACK
diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp
index 0004fc7e8e..add671701f 100644
--- a/esphome/core/time.cpp
+++ b/esphome/core/time.cpp
@@ -178,6 +178,15 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
   this->timestamp = res;
 }
 
+void ESPTime::recalc_timestamp_local(bool use_day_of_year) {
+  this->recalc_timestamp_utc(use_day_of_year);
+  this->timestamp -= ESPTime::timezone_offset();
+  ESPTime temp = ESPTime::from_epoch_local(this->timestamp);
+  if (temp.is_dst) {
+    this->timestamp -= 3600;
+  }
+}
+
 int32_t ESPTime::timezone_offset() {
   int32_t offset = 0;
   time_t now = ::time(nullptr);
diff --git a/esphome/core/time.h b/esphome/core/time.h
index 4300cf26b7..bce1108d93 100644
--- a/esphome/core/time.h
+++ b/esphome/core/time.h
@@ -99,6 +99,9 @@ struct ESPTime {
   /// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC).
   void recalc_timestamp_utc(bool use_day_of_year = true);
 
+  /// Recalculate the timestamp field from the other fields of this ESPTime instance assuming local fields.
+  void recalc_timestamp_local(bool use_day_of_year = true);
+
   /// Convert this ESPTime instance back to a tm struct.
   struct tm to_c_tm();
 
diff --git a/script/ci-custom.py b/script/ci-custom.py
index 27fcd480f5..abe004dba3 100755
--- a/script/ci-custom.py
+++ b/script/ci-custom.py
@@ -623,6 +623,7 @@ def lint_trailing_whitespace(fname, match):
         "esphome/components/cover/cover.h",
         "esphome/components/datetime/date_entity.h",
         "esphome/components/datetime/time_entity.h",
+        "esphome/components/datetime/datetime_entity.h",
         "esphome/components/display/display.h",
         "esphome/components/event/event.h",
         "esphome/components/fan/fan.h",
diff --git a/tests/components/datetime/test.all.yaml b/tests/components/datetime/test.all.yaml
index 3f5996bb8b..4e26b68121 100644
--- a/tests/components/datetime/test.all.yaml
+++ b/tests/components/datetime/test.all.yaml
@@ -1 +1,3 @@
 datetime:
+
+time:
diff --git a/tests/components/template/test.all.yaml b/tests/components/template/common.yaml
similarity index 88%
rename from tests/components/template/test.all.yaml
rename to tests/components/template/common.yaml
index 64faec36c2..ba7167157b 100644
--- a/tests/components/template/test.all.yaml
+++ b/tests/components/template/common.yaml
@@ -183,3 +183,25 @@ datetime:
             - x.hour
             - x.minute
             - x.second
+  - platform: template
+    name: DateTime
+    id: test_datetime
+    type: datetime
+    set_action:
+      - logger.log: "set_value"
+    on_value:
+      - logger.log:
+          format: "DateTime: %04d-%02d-%02d %02d:%02d:%02d"
+          args:
+            - x.year
+            - x.month
+            - x.day_of_month
+            - x.hour
+            - x.minute
+            - x.second
+
+time:
+  - platform: sntp  # Required for datetime
+
+wifi:  # Required for sntp time
+  ap:
diff --git a/tests/components/template/test.bk72xx.yaml b/tests/components/template/test.bk72xx.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/template/test.bk72xx.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/template/test.esp32-c3-idf.yaml b/tests/components/template/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/template/test.esp32-c3-idf.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/template/test.esp32-c3.yaml b/tests/components/template/test.esp32-c3.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/template/test.esp32-c3.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/template/test.esp32-idf.yaml b/tests/components/template/test.esp32-idf.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/template/test.esp32-idf.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/template/test.esp32-s3-idf.yaml b/tests/components/template/test.esp32-s3-idf.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/template/test.esp32-s3-idf.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/template/test.esp32.yaml b/tests/components/template/test.esp32.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/template/test.esp32.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/template/test.esp8266.yaml b/tests/components/template/test.esp8266.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/template/test.esp8266.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/template/test.rp2040.yaml b/tests/components/template/test.rp2040.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/template/test.rp2040.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml