mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 00:18:11 +01:00
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:
parent
3b6e8fa666
commit
76c5337987
37 changed files with 1251 additions and 23 deletions
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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_)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
156
esphome/components/datetime/time_entity.cpp
Normal file
156
esphome/components/datetime/time_entity.cpp
Normal 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
|
137
esphome/components/datetime/time_entity.h
Normal file
137
esphome/components/datetime/time_entity.h
Normal 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
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
68
esphome/components/mqtt/mqtt_time.cpp
Normal file
68
esphome/components/mqtt/mqtt_time.cpp
Normal 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
|
45
esphome/components/mqtt/mqtt_time.h
Normal file
45
esphome/components/mqtt/mqtt_time.h
Normal 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
|
|
@ -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(
|
||||||
|
|
111
esphome/components/template/datetime/template_time.cpp
Normal file
111
esphome/components/template/datetime/template_time.cpp
Normal 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
|
46
esphome/components/template/datetime/template_time.h
Normal file
46
esphome/components/template/datetime/template_time.h
Normal 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
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>'.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue