Add support for time entities (#6399)

* Add time entities

* Add tests

* Add myself to datetime codeowners

* Fix publishing times with 0 values

* Log performing TimeCall

* Implement `on_time` trigger

* Rename var

* Fix initial value for time

* Add arg name for clarity

* Remove useless checks
This commit is contained in:
Jesse Hills 2024-04-09 13:46:35 +12:00 committed by GitHub
parent 3b6e8fa666
commit 76c5337987
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1251 additions and 23 deletions

View File

@ -91,7 +91,7 @@ esphome/components/daikin_arc/* @MagicBear
esphome/components/daikin_brc/* @hagak esphome/components/daikin_brc/* @hagak
esphome/components/daly_bms/* @s1lvi0 esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core esphome/components/dashboard_import/* @esphome/core
esphome/components/datetime/* @rfdarter esphome/components/datetime/* @jesserockz @rfdarter
esphome/components/debug/* @OttoWinter esphome/components/debug/* @OttoWinter
esphome/components/delonghi/* @grob6000 esphome/components/delonghi/* @grob6000
esphome/components/dfplayer/* @glmnet esphome/components/dfplayer/* @glmnet

View File

@ -45,6 +45,7 @@ service APIConnection {
rpc lock_command (LockCommandRequest) returns (void) {} rpc lock_command (LockCommandRequest) returns (void) {}
rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
rpc date_command (DateCommandRequest) returns (void) {} rpc date_command (DateCommandRequest) returns (void) {}
rpc time_command (TimeCommandRequest) returns (void) {}
rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
@ -1658,3 +1659,44 @@ message DateCommandRequest {
uint32 month = 3; uint32 month = 3;
uint32 day = 4; uint32 day = 4;
} }
// ==================== DATETIME TIME ====================
message ListEntitiesTimeResponse {
option (id) = 103;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME";
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 TimeStateResponse {
option (id) = 104;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME";
option (no_delay) = true;
fixed32 key = 1;
// If the time does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 2;
uint32 hour = 3;
uint32 minute = 4;
uint32 second = 5;
}
message TimeCommandRequest {
option (id) = 105;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_TIME";
option (no_delay) = true;
fixed32 key = 1;
uint32 hour = 2;
uint32 minute = 3;
uint32 second = 4;
}

View File

@ -735,6 +735,43 @@ void APIConnection::date_command(const DateCommandRequest &msg) {
} }
#endif #endif
#ifdef USE_DATETIME_TIME
bool APIConnection::send_time_state(datetime::TimeEntity *time) {
if (!this->state_subscription_)
return false;
TimeStateResponse resp{};
resp.key = time->get_object_id_hash();
resp.missing_state = !time->has_state();
resp.hour = time->hour;
resp.minute = time->minute;
resp.second = time->second;
return this->send_time_state_response(resp);
}
bool APIConnection::send_time_info(datetime::TimeEntity *time) {
ListEntitiesTimeResponse msg;
msg.key = time->get_object_id_hash();
msg.object_id = time->get_object_id();
if (time->has_own_name())
msg.name = time->get_name();
msg.unique_id = get_default_unique_id("time", time);
msg.icon = time->get_icon();
msg.disabled_by_default = time->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(time->get_entity_category());
return this->send_list_entities_time_response(msg);
}
void APIConnection::time_command(const TimeCommandRequest &msg) {
datetime::TimeEntity *time = App.get_time_by_key(msg.key);
if (time == nullptr)
return;
auto call = time->make_call();
call.set_time(msg.hour, msg.minute, msg.second);
call.perform();
}
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool APIConnection::send_text_state(text::Text *text, std::string state) { bool APIConnection::send_text_state(text::Text *text, std::string state) {
if (!this->state_subscription_) if (!this->state_subscription_)

View File

@ -77,6 +77,11 @@ class APIConnection : public APIServerConnection {
bool send_date_info(datetime::DateEntity *date); bool send_date_info(datetime::DateEntity *date);
void date_command(const DateCommandRequest &msg) override; void date_command(const DateCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time);
bool send_time_info(datetime::TimeEntity *time);
void time_command(const TimeCommandRequest &msg) override;
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool send_text_state(text::Text *text, std::string state); bool send_text_state(text::Text *text, std::string state);
bool send_text_info(text::Text *text); bool send_text_info(text::Text *text);

View File

@ -7476,6 +7476,225 @@ void DateCommandRequest::dump_to(std::string &out) const {
out.append("}"); out.append("}");
} }
#endif #endif
bool ListEntitiesTimeResponse::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 ListEntitiesTimeResponse::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 ListEntitiesTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 2: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void ListEntitiesTimeResponse::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 ListEntitiesTimeResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("ListEntitiesTimeResponse {\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 TimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->missing_state = value.as_bool();
return true;
}
case 3: {
this->hour = value.as_uint32();
return true;
}
case 4: {
this->minute = value.as_uint32();
return true;
}
case 5: {
this->second = value.as_uint32();
return true;
}
default:
return false;
}
}
bool TimeStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void TimeStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_bool(2, this->missing_state);
buffer.encode_uint32(3, this->hour);
buffer.encode_uint32(4, this->minute);
buffer.encode_uint32(5, this->second);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void TimeStateResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("TimeStateResponse {\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(" hour: ");
sprintf(buffer, "%" PRIu32, this->hour);
out.append(buffer);
out.append("\n");
out.append(" minute: ");
sprintf(buffer, "%" PRIu32, this->minute);
out.append(buffer);
out.append("\n");
out.append(" second: ");
sprintf(buffer, "%" PRIu32, this->second);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->hour = value.as_uint32();
return true;
}
case 3: {
this->minute = value.as_uint32();
return true;
}
case 4: {
this->second = value.as_uint32();
return true;
}
default:
return false;
}
}
bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void TimeCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_uint32(2, this->hour);
buffer.encode_uint32(3, this->minute);
buffer.encode_uint32(4, this->second);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void TimeCommandRequest::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("TimeCommandRequest {\n");
out.append(" key: ");
sprintf(buffer, "%" PRIu32, this->key);
out.append(buffer);
out.append("\n");
out.append(" hour: ");
sprintf(buffer, "%" PRIu32, this->hour);
out.append(buffer);
out.append("\n");
out.append(" minute: ");
sprintf(buffer, "%" PRIu32, this->minute);
out.append(buffer);
out.append("\n");
out.append(" second: ");
sprintf(buffer, "%" PRIu32, this->second);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome

View File

@ -1919,6 +1919,56 @@ class DateCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesTimeResponse : 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 TimeStateResponse : public ProtoMessage {
public:
uint32_t key{0};
bool missing_state{false};
uint32_t hour{0};
uint32_t minute{0};
uint32_t second{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 TimeCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
uint32_t hour{0};
uint32_t minute{0};
uint32_t second{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;
};
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome

View File

@ -539,6 +539,24 @@ bool APIServerConnectionBase::send_date_state_response(const DateStateResponse &
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
#endif #endif
#ifdef USE_DATETIME_TIME
bool APIServerConnectionBase::send_list_entities_time_response(const ListEntitiesTimeResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_time_response: %s", msg.dump().c_str());
#endif
return this->send_message_<ListEntitiesTimeResponse>(msg, 103);
}
#endif
#ifdef USE_DATETIME_TIME
bool APIServerConnectionBase::send_time_state_response(const TimeStateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_time_state_response: %s", msg.dump().c_str());
#endif
return this->send_message_<TimeStateResponse>(msg, 104);
}
#endif
#ifdef USE_DATETIME_TIME
#endif
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) { switch (msg_type) {
case 1: { case 1: {
@ -979,6 +997,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_date_command_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_date_command_request: %s", msg.dump().c_str());
#endif #endif
this->on_date_command_request(msg); this->on_date_command_request(msg);
#endif
break;
}
case 105: {
#ifdef USE_DATETIME_TIME
TimeCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_time_command_request: %s", msg.dump().c_str());
#endif
this->on_time_command_request(msg);
#endif #endif
break; break;
} }
@ -1279,6 +1308,19 @@ void APIServerConnection::on_date_command_request(const DateCommandRequest &msg)
this->date_command(msg); this->date_command(msg);
} }
#endif #endif
#ifdef USE_DATETIME_TIME
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->time_command(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &msg) { const SubscribeBluetoothLEAdvertisementsRequest &msg) {

View File

@ -270,6 +270,15 @@ class APIServerConnectionBase : public ProtoService {
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
virtual void on_date_command_request(const DateCommandRequest &value){}; virtual void on_date_command_request(const DateCommandRequest &value){};
#endif
#ifdef USE_DATETIME_TIME
bool send_list_entities_time_response(const ListEntitiesTimeResponse &msg);
#endif
#ifdef USE_DATETIME_TIME
bool send_time_state_response(const TimeStateResponse &msg);
#endif
#ifdef USE_DATETIME_TIME
virtual void on_time_command_request(const TimeCommandRequest &value){};
#endif #endif
protected: protected:
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
@ -328,6 +337,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
virtual void date_command(const DateCommandRequest &msg) = 0; virtual void date_command(const DateCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_DATETIME_TIME
virtual void time_command(const TimeCommandRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
#endif #endif
@ -417,6 +429,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
void on_date_command_request(const DateCommandRequest &msg) override; void on_date_command_request(const DateCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_TIME
void on_time_command_request(const TimeCommandRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
#endif #endif

View File

@ -264,6 +264,15 @@ void APIServer::on_date_update(datetime::DateEntity *obj) {
} }
#endif #endif
#ifdef USE_DATETIME_TIME
void APIServer::on_time_update(datetime::TimeEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_time_state(obj);
}
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
void APIServer::on_text_update(text::Text *obj, const std::string &state) { void APIServer::on_text_update(text::Text *obj, const std::string &state) {
if (obj->is_internal()) if (obj->is_internal())

View File

@ -69,6 +69,9 @@ class APIServer : public Component, public Controller {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
void on_date_update(datetime::DateEntity *obj) override; void on_date_update(datetime::DateEntity *obj) override;
#endif #endif
#ifdef USE_DATETIME_TIME
void on_time_update(datetime::TimeEntity *obj) override;
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
void on_text_update(text::Text *obj, const std::string &state) override; void on_text_update(text::Text *obj, const std::string &state) override;
#endif #endif

View File

@ -64,6 +64,10 @@ bool ListEntitiesIterator::on_number(number::Number *number) { return this->clie
bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_info(date); } bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_info(date); }
#endif #endif
#ifdef USE_DATETIME_TIME
bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_info(time); }
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool ListEntitiesIterator::on_text(text::Text *text) { return this->client_->send_text_info(text); } bool ListEntitiesIterator::on_text(text::Text *text) { return this->client_->send_text_info(text); }
#endif #endif

View File

@ -49,6 +49,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool on_date(datetime::DateEntity *date) override; bool on_date(datetime::DateEntity *date) override;
#endif #endif
#ifdef USE_DATETIME_TIME
bool on_time(datetime::TimeEntity *time) override;
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool on_text(text::Text *text) override; bool on_text(text::Text *text) override;
#endif #endif

View File

@ -45,6 +45,9 @@ bool InitialStateIterator::on_number(number::Number *number) {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_state(date); } bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_state(date); }
#endif #endif
#ifdef USE_DATETIME_TIME
bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); }
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text, text->state); } bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text, text->state); }
#endif #endif

View File

@ -46,6 +46,9 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool on_date(datetime::DateEntity *date) override; bool on_date(datetime::DateEntity *date) override;
#endif #endif
#ifdef USE_DATETIME_TIME
bool on_time(datetime::TimeEntity *time) override;
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool on_text(text::Text *text) override; bool on_text(text::Text *text) override;
#endif #endif

View File

@ -3,38 +3,50 @@ import esphome.codegen as cg
# import cpp_generator as cpp # import cpp_generator as cpp
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.components import mqtt from esphome.components import mqtt, time
from esphome.const import ( from esphome.const import (
CONF_ID, CONF_ID,
CONF_ON_TIME,
CONF_ON_VALUE, CONF_ON_VALUE,
CONF_TIME_ID,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_TYPE, CONF_TYPE,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_DATE, CONF_DATE,
CONF_TIME,
CONF_YEAR, CONF_YEAR,
CONF_MONTH, CONF_MONTH,
CONF_DAY, CONF_DAY,
CONF_SECOND,
CONF_HOUR,
CONF_MINUTE,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@rfdarter"] CODEOWNERS = ["@rfdarter", "@jesserockz"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
datetime_ns = cg.esphome_ns.namespace("datetime") datetime_ns = cg.esphome_ns.namespace("datetime")
DateTimeBase = datetime_ns.class_("DateTimeBase", cg.EntityBase) DateTimeBase = datetime_ns.class_("DateTimeBase", cg.EntityBase)
DateEntity = datetime_ns.class_("DateEntity", DateTimeBase) DateEntity = datetime_ns.class_("DateEntity", DateTimeBase)
TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase)
# Actions # Actions
DateSetAction = datetime_ns.class_("DateSetAction", automation.Action) DateSetAction = datetime_ns.class_("DateSetAction", automation.Action)
TimeSetAction = datetime_ns.class_("TimeSetAction", automation.Action)
DateTimeStateTrigger = datetime_ns.class_( DateTimeStateTrigger = datetime_ns.class_(
"DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime) "DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime)
) )
OnTimeTrigger = datetime_ns.class_(
"OnTimeTrigger", automation.Trigger, cg.Component, cg.Parented.template(TimeEntity)
)
DATETIME_MODES = [ DATETIME_MODES = [
"DATE", "DATE",
"TIME", "TIME",
@ -44,7 +56,6 @@ DATETIME_MODES = [
_DATETIME_SCHEMA = cv.Schema( _DATETIME_SCHEMA = cv.Schema(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDatetimeComponent),
cv.Optional(CONF_ON_VALUE): automation.validate_automation( cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger),
@ -57,6 +68,7 @@ _DATETIME_SCHEMA = cv.Schema(
def date_schema(class_: MockObjClass) -> cv.Schema: def date_schema(class_: MockObjClass) -> cv.Schema:
schema = { schema = {
cv.GenerateID(): cv.declare_id(class_), 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), cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True),
} }
return _DATETIME_SCHEMA.extend(schema) return _DATETIME_SCHEMA.extend(schema)
@ -65,7 +77,20 @@ def date_schema(class_: MockObjClass) -> cv.Schema:
def time_schema(class_: MockObjClass) -> cv.Schema: def time_schema(class_: MockObjClass) -> cv.Schema:
schema = { schema = {
cv.GenerateID(): cv.declare_id(class_), 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_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
),
} }
return _DATETIME_SCHEMA.extend(schema) return _DATETIME_SCHEMA.extend(schema)
@ -88,6 +113,17 @@ async def setup_datetime_core_(var, config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.ESPTime, "x")], conf) 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)
for conf in config.get(CONF_ON_TIME, []):
assert rtc is not None
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], rtc)
await automation.build_automation(trigger, [], conf)
await cg.register_component(trigger, conf)
await cg.register_parented(trigger, var)
async def register_datetime(var, config): async def register_datetime(var, config):
if not CORE.has_id(config[CONF_ID]): if not CORE.has_id(config[CONF_ID]):
@ -109,18 +145,12 @@ async def to_code(config):
cg.add_global(datetime_ns.using) cg.add_global(datetime_ns.using)
OPERATION_BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(DateEntity),
}
)
@automation.register_action( @automation.register_action(
"datetime.date.set", "datetime.date.set",
DateSetAction, DateSetAction,
OPERATION_BASE_SCHEMA.extend( cv.Schema(
{ {
cv.Required(CONF_ID): cv.use_id(DateEntity),
cv.Required(CONF_DATE): cv.Any( cv.Required(CONF_DATE): cv.Any(
cv.returning_lambda, cv.date_time(allowed_time=False) cv.returning_lambda, cv.date_time(allowed_time=False)
), ),
@ -144,3 +174,34 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args):
) )
cg.add(action_var.set_date(date_struct)) cg.add(action_var.set_date(date_struct))
return action_var return action_var
@automation.register_action(
"datetime.time.set",
TimeSetAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(TimeEntity),
cv.Required(CONF_TIME): cv.Any(
cv.returning_lambda, cv.date_time(allowed_date=False)
),
}
),
)
async def datetime_time_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])
time_config = config[CONF_TIME]
if cg.is_template(time_config):
template_ = await cg.templatable(config[CONF_TIME], [], cg.ESPTime)
cg.add(action_var.set_time(template_))
else:
time_struct = cg.StructInitializer(
cg.ESPTime,
("second", time_config[CONF_SECOND]),
("minute", time_config[CONF_MINUTE]),
("hour", time_config[CONF_HOUR]),
)
cg.add(action_var.set_time(time_struct))
return action_var

View File

@ -0,0 +1,156 @@
#include "time_entity.h"
#ifdef USE_DATETIME_TIME
#include "esphome/core/log.h"
namespace esphome {
namespace datetime {
static const char *const TAG = "datetime.time_entity";
void TimeEntity::publish_state() {
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 time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
this->second_);
this->state_callback_.call();
}
TimeCall TimeEntity::make_call() { return TimeCall(this); }
void TimeCall::validate_() {
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 TimeCall::perform() {
this->validate_();
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
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);
}
TimeCall &TimeCall::set_time(uint8_t hour, uint8_t minute, uint8_t second) {
this->hour_ = hour;
this->minute_ = minute;
this->second_ = second;
return *this;
};
TimeCall &TimeCall::set_time(ESPTime time) { return this->set_time(time.hour, time.minute, time.second); };
TimeCall &TimeCall::set_time(const std::string &time) {
ESPTime val{};
if (!ESPTime::strptime(time, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this;
}
return this->set_time(val);
}
TimeCall TimeEntityRestoreState::to_call(TimeEntity *time) {
TimeCall call = time->make_call();
call.set_time(this->hour, this->minute, this->second);
return call;
}
void TimeEntityRestoreState::apply(TimeEntity *time) {
time->hour_ = this->hour;
time->minute_ = this->minute;
time->second_ = this->second;
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
void OnTimeTrigger::loop() {
if (!this->parent_->has_state()) {
return;
}
ESPTime time = this->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", time.second, time.minute, time.hour);
}
if (this->matches_(time))
this->trigger();
}
bool OnTimeTrigger::matches_(const ESPTime &time) const {
return time.is_valid() && time.hour == this->parent_->hour && time.minute == this->parent_->minute &&
time.second == this->parent_->second;
}
#endif
} // namespace datetime
} // namespace esphome
#endif // USE_DATETIME_TIME

View File

@ -0,0 +1,137 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_DATETIME_TIME
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "esphome/core/time.h"
#include "datetime_base.h"
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
namespace esphome {
namespace datetime {
#define LOG_DATETIME_TIME(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 TimeCall;
class TimeEntity;
struct TimeEntityRestoreState {
uint8_t hour;
uint8_t minute;
uint8_t second;
TimeCall to_call(TimeEntity *time);
void apply(TimeEntity *time);
} __attribute__((packed));
class TimeEntity : public DateTimeBase {
protected:
uint8_t hour_;
uint8_t minute_;
uint8_t second_;
public:
void publish_state();
TimeCall make_call();
ESPTime state_as_esptime() const override {
ESPTime obj;
obj.hour = this->hour_;
obj.minute = this->minute_;
obj.second = this->second_;
return obj;
}
const uint8_t &hour = hour_;
const uint8_t &minute = minute_;
const uint8_t &second = second_;
protected:
friend class TimeCall;
friend struct TimeEntityRestoreState;
virtual void control(const TimeCall &call) = 0;
};
class TimeCall {
public:
explicit TimeCall(TimeEntity *parent) : parent_(parent) {}
void perform();
TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second);
TimeCall &set_time(ESPTime time);
TimeCall &set_time(const std::string &time);
TimeCall &set_hour(uint8_t hour) {
this->hour_ = hour;
return *this;
}
TimeCall &set_minute(uint8_t minute) {
this->minute_ = minute;
return *this;
}
TimeCall &set_second(uint8_t second) {
this->second_ = second;
return *this;
}
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_();
TimeEntity *parent_;
optional<uint8_t> hour_;
optional<uint8_t> minute_;
optional<uint8_t> second_;
};
template<typename... Ts> class TimeSetAction : public Action<Ts...>, public Parented<TimeEntity> {
public:
TEMPLATABLE_VALUE(ESPTime, time)
void play(Ts... x) override {
auto call = this->parent_->make_call();
if (this->time_.has_value()) {
call.set_time(this->time_.value(x...));
}
call.perform();
}
};
#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
#endif // USE_DATETIME_TIME

View File

@ -113,7 +113,8 @@ MQTTSensorComponent = mqtt_ns.class_("MQTTSensorComponent", MQTTComponent)
MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent) MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent)
MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent) MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent)
MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent) MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent)
MQTTDatetimeComponent = mqtt_ns.class_("MQTTDatetimeComponent", MQTTComponent) MQTTDateComponent = mqtt_ns.class_("MQTTDateComponent", MQTTComponent)
MQTTTimeComponent = mqtt_ns.class_("MQTTTimeComponent", MQTTComponent)
MQTTTextComponent = mqtt_ns.class_("MQTTTextComponent", MQTTComponent) MQTTTextComponent = mqtt_ns.class_("MQTTTextComponent", MQTTComponent)
MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent) MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent)
MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent) MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent)

View File

@ -13,9 +13,9 @@ namespace mqtt {
class MQTTDateComponent : public mqtt::MQTTComponent { class MQTTDateComponent : public mqtt::MQTTComponent {
public: public:
/** Construct this MQTTDatetimeComponent instance with the provided friendly_name and datetime /** Construct this MQTTDateComponent instance with the provided friendly_name and date
* *
* @param datetime The datetime component. * @param date The date component.
*/ */
explicit MQTTDateComponent(datetime::DateEntity *date); explicit MQTTDateComponent(datetime::DateEntity *date);

View File

@ -0,0 +1,68 @@
#include "mqtt_time.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;
MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {}
void MQTTTimeComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
auto call = this->time_->make_call();
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->time_->add_on_state_callback(
[this]() { this->publish_state(this->time_->hour, this->time_->minute, this->time_->second); });
}
void MQTTTimeComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Time '%s':", this->time_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
}
std::string MQTTTimeComponent::component_type() const { return "time"; }
const EntityBase *MQTTTimeComponent::get_entity() const { return this->time_; }
void MQTTTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// Nothing extra to add here
}
bool MQTTTimeComponent::send_initial_state() {
if (this->time_->has_state()) {
return this->publish_state(this->time_->hour, this->time_->minute, this->time_->second);
} else {
return true;
}
}
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
root["hour"] = hour;
root["minute"] = minute;
root["second"] = second;
});
}
} // namespace mqtt
} // namespace esphome
#endif // USE_DATETIME_TIME
#endif // USE_MQTT

View File

@ -0,0 +1,45 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_MQTT
#ifdef USE_DATETIME_TIME
#include "esphome/components/datetime/time_entity.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTTimeComponent : public mqtt::MQTTComponent {
public:
/** Construct this MQTTTimeComponent instance with the provided friendly_name and time
*
* @param time The time entity.
*/
explicit MQTTTimeComponent(datetime::TimeEntity *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(uint8_t hour, uint8_t minute, uint8_t second);
protected:
std::string component_type() const override;
const EntityBase *get_entity() const override;
datetime::TimeEntity *time_;
};
} // namespace mqtt
} // namespace esphome
#endif // USE_DATETIME_DATE
#endif // USE_MQTT

View File

@ -9,7 +9,11 @@ from esphome.const import (
CONF_RESTORE_VALUE, CONF_RESTORE_VALUE,
CONF_SET_ACTION, CONF_SET_ACTION,
CONF_DAY, CONF_DAY,
CONF_HOUR,
CONF_MINUTE,
CONF_MONTH, CONF_MONTH,
CONF_SECOND,
CONF_TYPE,
CONF_YEAR, CONF_YEAR,
) )
@ -23,6 +27,10 @@ TemplateDate = template_ns.class_(
"TemplateDate", datetime.DateEntity, cg.PollingComponent "TemplateDate", datetime.DateEntity, cg.PollingComponent
) )
TemplateTime = template_ns.class_(
"TemplateTime", datetime.TimeEntity, cg.PollingComponent
)
def validate(config): def validate(config):
config = config.copy() config = config.copy()
@ -63,6 +71,13 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_INITIAL_VALUE): cv.date_time(allowed_time=False), cv.Optional(CONF_INITIAL_VALUE): cv.date_time(allowed_time=False),
} }
), ),
"TIME": datetime.time_schema(TemplateTime)
.extend(_BASE_SCHEMA)
.extend(
{
cv.Optional(CONF_INITIAL_VALUE): cv.date_time(allowed_date=False),
}
),
}, },
upper=True, upper=True,
), ),
@ -85,13 +100,22 @@ async def to_code(config):
cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE]))
if initial_value := config.get(CONF_INITIAL_VALUE): if initial_value := config.get(CONF_INITIAL_VALUE):
date_struct = cg.StructInitializer( if config[CONF_TYPE] == "DATE":
cg.ESPTime, date_struct = cg.StructInitializer(
("day_of_month", initial_value[CONF_DAY]), cg.ESPTime,
("month", initial_value[CONF_MONTH]), ("day_of_month", initial_value[CONF_DAY]),
("year", initial_value[CONF_YEAR]), ("month", initial_value[CONF_MONTH]),
) ("year", initial_value[CONF_YEAR]),
cg.add(var.set_initial_value(date_struct)) )
cg.add(var.set_initial_value(date_struct))
elif config[CONF_TYPE] == "TIME":
time_struct = cg.StructInitializer(
cg.ESPTime,
("second", initial_value[CONF_SECOND]),
("minute", initial_value[CONF_MINUTE]),
("hour", initial_value[CONF_HOUR]),
)
cg.add(var.set_initial_value(time_struct))
if CONF_SET_ACTION in config: if CONF_SET_ACTION in config:
await automation.build_automation( await automation.build_automation(

View File

@ -0,0 +1,111 @@
#include "template_time.h"
#ifdef USE_DATETIME_TIME
#include "esphome/core/log.h"
namespace esphome {
namespace template_ {
static const char *const TAG = "template.time";
void TemplateTime::setup() {
if (this->f_.has_value())
return;
ESPTime state{};
if (!this->restore_value_) {
state = this->initial_value_;
} else {
datetime::TimeEntityRestoreState temp;
this->pref_ =
global_preferences->make_preference<datetime::TimeEntityRestoreState>(194434060U ^ 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->hour_ = state.hour;
this->minute_ = state.minute;
this->second_ = state.second;
this->publish_state();
}
void TemplateTime::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
}
void TemplateTime::control(const datetime::TimeCall &call) {
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_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_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::TimeEntityRestoreState temp = {};
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 TemplateTime::dump_config() {
LOG_DATETIME_TIME("", "Template Time", this);
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
LOG_UPDATE_INTERVAL(this);
}
} // namespace template_
} // namespace esphome
#endif // USE_DATETIME_TIME

View File

@ -0,0 +1,46 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_DATETIME_TIME
#include "esphome/components/datetime/time_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 TemplateTime : public datetime::TimeEntity, 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::TimeCall &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

View File

@ -113,6 +113,13 @@ bool ListEntitiesIterator::on_date(datetime::DateEntity *date) {
} }
#endif #endif
#ifdef USE_DATETIME_TIME
bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) {
this->web_server_->events_.send(this->web_server_->time_json(time, DETAIL_ALL).c_str(), "state");
return true;
}
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool ListEntitiesIterator::on_text(text::Text *text) { bool ListEntitiesIterator::on_text(text::Text *text) {
if (this->web_server_->events_.count() == 0) if (this->web_server_->events_.count() == 0)

View File

@ -44,6 +44,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool on_date(datetime::DateEntity *date) override; bool on_date(datetime::DateEntity *date) override;
#endif #endif
#ifdef USE_DATETIME_TIME
bool on_time(datetime::TimeEntity *time) override;
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool on_text(text::Text *text) override; bool on_text(text::Text *text) override;
#endif #endif

View File

@ -921,6 +921,52 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con
} }
#endif // USE_DATETIME_DATE #endif // USE_DATETIME_DATE
#ifdef USE_DATETIME_TIME
void WebServer::on_time_update(datetime::TimeEntity *obj) {
this->events_.send(this->time_json(obj, DETAIL_STATE).c_str(), "state");
}
void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_times()) {
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET && match.method.empty()) {
std::string data = this->time_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_time(value);
}
this->schedule_([call]() mutable { call.perform(); });
request->send(200);
return;
}
request->send(404);
}
std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) {
set_json_id(root, obj, "time-" + obj->get_object_id(), start_config);
std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second);
root["value"] = value;
root["state"] = value;
});
}
#endif // USE_DATETIME_TIME
#ifdef USE_TEXT #ifdef USE_TEXT
void WebServer::on_text_update(text::Text *obj, const std::string &state) { void WebServer::on_text_update(text::Text *obj, const std::string &state) {
if (this->events_.count() == 0) if (this->events_.count() == 0)
@ -1320,6 +1366,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
return true; return true;
#endif #endif
#ifdef USE_DATETIME_TIME
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "time")
return true;
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "text") if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "text")
return true; return true;
@ -1445,6 +1496,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
} }
#endif #endif
#ifdef USE_DATETIME_TIME
if (match.domain == "time") {
this->handle_time_request(request, match);
return;
}
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
if (match.domain == "text") { if (match.domain == "text") {
this->handle_text_request(request, match); this->handle_text_request(request, match);

View File

@ -8,9 +8,9 @@
#include <vector> #include <vector>
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <deque>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <deque>
#endif #endif
#if USE_WEBSERVER_VERSION >= 2 #if USE_WEBSERVER_VERSION >= 2
@ -230,6 +230,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
std::string date_json(datetime::DateEntity *obj, JsonDetail start_config); std::string date_json(datetime::DateEntity *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_DATETIME_TIME
void on_time_update(datetime::TimeEntity *obj) override;
/// Handle a time request under '/time/<id>'.
void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match);
/// Dump the time state with its value as a JSON string.
std::string time_json(datetime::TimeEntity *obj, JsonDetail start_config);
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
void on_text_update(text::Text *obj, const std::string &state) override; void on_text_update(text::Text *obj, const std::string &state) override;
/// Handle a text input request under '/text/<id>'. /// Handle a text input request under '/text/<id>'.

View File

@ -42,6 +42,9 @@
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
#include "esphome/components/datetime/date_entity.h" #include "esphome/components/datetime/date_entity.h"
#endif #endif
#ifdef USE_DATETIME_TIME
#include "esphome/components/datetime/time_entity.h"
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
#include "esphome/components/text/text.h" #include "esphome/components/text/text.h"
#endif #endif
@ -128,6 +131,10 @@ class Application {
void register_date(datetime::DateEntity *date) { this->dates_.push_back(date); } void register_date(datetime::DateEntity *date) { this->dates_.push_back(date); }
#endif #endif
#ifdef USE_DATETIME_TIME
void register_time(datetime::TimeEntity *time) { this->times_.push_back(time); }
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
void register_text(text::Text *text) { this->texts_.push_back(text); } void register_text(text::Text *text) { this->texts_.push_back(text); }
#endif #endif
@ -305,6 +312,15 @@ class Application {
return nullptr; return nullptr;
} }
#endif #endif
#ifdef USE_DATETIME_TIME
const std::vector<datetime::TimeEntity *> &get_times() { return this->times_; }
datetime::TimeEntity *get_time_by_key(uint32_t key, bool include_internal = false) {
for (auto *obj : this->times_)
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
return nullptr;
}
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
const std::vector<text::Text *> &get_texts() { return this->texts_; } const std::vector<text::Text *> &get_texts() { return this->texts_; }
text::Text *get_text_by_key(uint32_t key, bool include_internal = false) { text::Text *get_text_by_key(uint32_t key, bool include_internal = false) {
@ -401,6 +417,9 @@ class Application {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
std::vector<datetime::DateEntity *> dates_{}; std::vector<datetime::DateEntity *> dates_{};
#endif #endif
#ifdef USE_DATETIME_TIME
std::vector<datetime::TimeEntity *> times_{};
#endif
#ifdef USE_SELECT #ifdef USE_SELECT
std::vector<select::Select *> selects_{}; std::vector<select::Select *> selects_{};
#endif #endif

View File

@ -217,6 +217,21 @@ void ComponentIterator::advance() {
} }
break; break;
#endif #endif
#ifdef USE_DATETIME_TIME
case IteratorState::DATETIME_TIME:
if (this->at_ >= App.get_times().size()) {
advance_platform = true;
} else {
auto *time = App.get_times()[this->at_];
if (time->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_time(time);
}
}
break;
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
case IteratorState::TEXT: case IteratorState::TEXT:
if (this->at_ >= App.get_texts().size()) { if (this->at_ >= App.get_texts().size()) {

View File

@ -60,6 +60,9 @@ class ComponentIterator {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
virtual bool on_date(datetime::DateEntity *date) = 0; virtual bool on_date(datetime::DateEntity *date) = 0;
#endif #endif
#ifdef USE_DATETIME_TIME
virtual bool on_time(datetime::TimeEntity *time) = 0;
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
virtual bool on_text(text::Text *text) = 0; virtual bool on_text(text::Text *text) = 0;
#endif #endif
@ -120,6 +123,9 @@ class ComponentIterator {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
DATETIME_DATE, DATETIME_DATE,
#endif #endif
#ifdef USE_DATETIME_TIME
DATETIME_TIME,
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
TEXT, TEXT,
#endif #endif

View File

@ -65,6 +65,12 @@ void Controller::setup_controller(bool include_internal) {
obj->add_on_state_callback([this, obj]() { this->on_date_update(obj); }); obj->add_on_state_callback([this, obj]() { this->on_date_update(obj); });
} }
#endif #endif
#ifdef USE_DATETIME_TIME
for (auto *obj : App.get_times()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_time_update(obj); });
}
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
for (auto *obj : App.get_texts()) { for (auto *obj : App.get_texts()) {
if (include_internal || !obj->is_internal()) if (include_internal || !obj->is_internal())

View File

@ -34,6 +34,9 @@
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
#include "esphome/components/datetime/date_entity.h" #include "esphome/components/datetime/date_entity.h"
#endif #endif
#ifdef USE_DATETIME_TIME
#include "esphome/components/datetime/time_entity.h"
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
#include "esphome/components/text/text.h" #include "esphome/components/text/text.h"
#endif #endif
@ -85,6 +88,9 @@ class Controller {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
virtual void on_date_update(datetime::DateEntity *obj){}; virtual void on_date_update(datetime::DateEntity *obj){};
#endif #endif
#ifdef USE_DATETIME_TIME
virtual void on_time_update(datetime::TimeEntity *obj){};
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
virtual void on_text_update(text::Text *obj, const std::string &state){}; virtual void on_text_update(text::Text *obj, const std::string &state){};
#endif #endif

View File

@ -36,6 +36,7 @@
#define USE_NUMBER #define USE_NUMBER
#define USE_DATETIME #define USE_DATETIME
#define USE_DATETIME_DATE #define USE_DATETIME_DATE
#define USE_DATETIME_TIME
#define USE_OTA #define USE_OTA
#define USE_OTA_PASSWORD #define USE_OTA_PASSWORD
#define USE_OTA_STATE_CALLBACK #define USE_OTA_STATE_CALLBACK

View File

@ -622,6 +622,7 @@ def lint_trailing_whitespace(fname, match):
"esphome/components/climate/climate.h", "esphome/components/climate/climate.h",
"esphome/components/cover/cover.h", "esphome/components/cover/cover.h",
"esphome/components/datetime/date_entity.h", "esphome/components/datetime/date_entity.h",
"esphome/components/datetime/time_entity.h",
"esphome/components/display/display.h", "esphome/components/display/display.h",
"esphome/components/fan/fan.h", "esphome/components/fan/fan.h",
"esphome/components/i2c/i2c.h", "esphome/components/i2c/i2c.h",

View File

@ -153,3 +153,16 @@ datetime:
- x.year - x.year
- x.month - x.month
- x.day_of_month - x.day_of_month
- platform: template
name: Time
id: test_time
type: time
set_action:
- logger.log: "set_value"
on_value:
- logger.log:
format: "Time: %02d:%02d:%02d"
args:
- x.hour
- x.minute
- x.second