1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-04 20:32:21 +01:00

Merge branch 'dev' into nrf52

This commit is contained in:
tomaszduda23
2024-04-25 23:24:12 +02:00
committed by GitHub
57 changed files with 1462 additions and 75 deletions

View File

@@ -47,6 +47,7 @@ service APIConnection {
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 time_command (TimeCommandRequest) returns (void) {}
rpc datetime_command (DateTimeCommandRequest) 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) {}
@@ -1777,3 +1778,40 @@ message ValveCommandRequest {
float position = 3; float position = 3;
bool stop = 4; bool stop = 4;
} }
// ==================== DATETIME DATETIME ====================
message ListEntitiesDateTimeResponse {
option (id) = 112;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
}
message DateTimeStateResponse {
option (id) = 113;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME";
option (no_delay) = true;
fixed32 key = 1;
// If the datetime does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 2;
fixed32 epoch_seconds = 3;
}
message DateTimeCommandRequest {
option (id) = 114;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_DATETIME";
option (no_delay) = true;
fixed32 key = 1;
fixed32 epoch_seconds = 2;
}

View File

@@ -772,6 +772,44 @@ void APIConnection::time_command(const TimeCommandRequest &msg) {
} }
#endif #endif
#ifdef USE_DATETIME_DATETIME
bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
if (!this->state_subscription_)
return false;
DateTimeStateResponse resp{};
resp.key = datetime->get_object_id_hash();
resp.missing_state = !datetime->has_state();
if (datetime->has_state()) {
ESPTime state = datetime->state_as_esptime();
resp.epoch_seconds = state.timestamp;
}
return this->send_date_time_state_response(resp);
}
bool APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) {
ListEntitiesDateTimeResponse msg;
msg.key = datetime->get_object_id_hash();
msg.object_id = datetime->get_object_id();
if (datetime->has_own_name())
msg.name = datetime->get_name();
msg.unique_id = get_default_unique_id("datetime", datetime);
msg.icon = datetime->get_icon();
msg.disabled_by_default = datetime->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(datetime->get_entity_category());
return this->send_list_entities_date_time_response(msg);
}
void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key);
if (datetime == nullptr)
return;
auto call = datetime->make_call();
call.set_datetime(msg.epoch_seconds);
call.perform();
}
#endif
#ifdef USE_TEXT #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

@@ -82,6 +82,11 @@ class APIConnection : public APIServerConnection {
bool send_time_info(datetime::TimeEntity *time); bool send_time_info(datetime::TimeEntity *time);
void time_command(const TimeCommandRequest &msg) override; void time_command(const TimeCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime);
bool send_datetime_info(datetime::DateTimeEntity *datetime);
void datetime_command(const DateTimeCommandRequest &msg) override;
#endif
#ifdef USE_TEXT #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

@@ -8093,6 +8093,179 @@ void ValveCommandRequest::dump_to(std::string &out) const {
out.append("}"); out.append("}");
} }
#endif #endif
bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 6: {
this->disabled_by_default = value.as_bool();
return true;
}
case 7: {
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
default:
return false;
}
}
bool ListEntitiesDateTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->object_id = value.as_string();
return true;
}
case 3: {
this->name = value.as_string();
return true;
}
case 4: {
this->unique_id = value.as_string();
return true;
}
case 5: {
this->icon = value.as_string();
return true;
}
default:
return false;
}
}
bool ListEntitiesDateTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 2: {
this->key = value.as_fixed32();
return true;
}
default:
return false;
}
}
void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name);
buffer.encode_string(4, this->unique_id);
buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesDateTimeResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("ListEntitiesDateTimeResponse {\n");
out.append(" object_id: ");
out.append("'").append(this->object_id).append("'");
out.append("\n");
out.append(" key: ");
sprintf(buffer, "%" PRIu32, this->key);
out.append(buffer);
out.append("\n");
out.append(" name: ");
out.append("'").append(this->name).append("'");
out.append("\n");
out.append(" unique_id: ");
out.append("'").append(this->unique_id).append("'");
out.append("\n");
out.append(" icon: ");
out.append("'").append(this->icon).append("'");
out.append("\n");
out.append(" disabled_by_default: ");
out.append(YESNO(this->disabled_by_default));
out.append("\n");
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append("}");
}
#endif
bool DateTimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {
this->missing_state = value.as_bool();
return true;
}
default:
return false;
}
}
bool DateTimeStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
case 3: {
this->epoch_seconds = value.as_fixed32();
return true;
}
default:
return false;
}
}
void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_bool(2, this->missing_state);
buffer.encode_fixed32(3, this->epoch_seconds);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void DateTimeStateResponse::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("DateTimeStateResponse {\n");
out.append(" key: ");
sprintf(buffer, "%" PRIu32, this->key);
out.append(buffer);
out.append("\n");
out.append(" missing_state: ");
out.append(YESNO(this->missing_state));
out.append("\n");
out.append(" epoch_seconds: ");
sprintf(buffer, "%" PRIu32, this->epoch_seconds);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1: {
this->key = value.as_fixed32();
return true;
}
case 2: {
this->epoch_seconds = value.as_fixed32();
return true;
}
default:
return false;
}
}
void DateTimeCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_fixed32(2, this->epoch_seconds);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void DateTimeCommandRequest::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("DateTimeCommandRequest {\n");
out.append(" key: ");
sprintf(buffer, "%" PRIu32, this->key);
out.append(buffer);
out.append("\n");
out.append(" epoch_seconds: ");
sprintf(buffer, "%" PRIu32, this->epoch_seconds);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome

View File

@@ -2060,6 +2060,51 @@ class ValveCommandRequest : 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 ListEntitiesDateTimeResponse : public ProtoMessage {
public:
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class DateTimeStateResponse : public ProtoMessage {
public:
uint32_t key{0};
bool missing_state{false};
uint32_t epoch_seconds{0};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class DateTimeCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
uint32_t epoch_seconds{0};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
};
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome

View File

@@ -591,6 +591,24 @@ bool APIServerConnectionBase::send_valve_state_response(const ValveStateResponse
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
#endif #endif
#ifdef USE_DATETIME_DATETIME
bool APIServerConnectionBase::send_list_entities_date_time_response(const ListEntitiesDateTimeResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_date_time_response: %s", msg.dump().c_str());
#endif
return this->send_message_<ListEntitiesDateTimeResponse>(msg, 112);
}
#endif
#ifdef USE_DATETIME_DATETIME
bool APIServerConnectionBase::send_date_time_state_response(const DateTimeStateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_date_time_state_response: %s", msg.dump().c_str());
#endif
return this->send_message_<DateTimeStateResponse>(msg, 113);
}
#endif
#ifdef USE_DATETIME_DATETIME
#endif
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { 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: {
@@ -1064,6 +1082,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_valve_command_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_valve_command_request: %s", msg.dump().c_str());
#endif #endif
this->on_valve_command_request(msg); this->on_valve_command_request(msg);
#endif
break;
}
case 114: {
#ifdef USE_DATETIME_DATETIME
DateTimeCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str());
#endif
this->on_date_time_command_request(msg);
#endif #endif
break; break;
} }
@@ -1379,6 +1408,19 @@ void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg)
this->time_command(msg); this->time_command(msg);
} }
#endif #endif
#ifdef USE_DATETIME_DATETIME
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->datetime_command(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY #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

@@ -294,6 +294,15 @@ class APIServerConnectionBase : public ProtoService {
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
virtual void on_valve_command_request(const ValveCommandRequest &value){}; virtual void on_valve_command_request(const ValveCommandRequest &value){};
#endif
#ifdef USE_DATETIME_DATETIME
bool send_list_entities_date_time_response(const ListEntitiesDateTimeResponse &msg);
#endif
#ifdef USE_DATETIME_DATETIME
bool send_date_time_state_response(const DateTimeStateResponse &msg);
#endif
#ifdef USE_DATETIME_DATETIME
virtual void on_date_time_command_request(const DateTimeCommandRequest &value){};
#endif #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;
@@ -358,6 +367,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
virtual void time_command(const TimeCommandRequest &msg) = 0; virtual void time_command(const TimeCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_DATETIME_DATETIME
virtual void datetime_command(const DateTimeCommandRequest &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
@@ -453,6 +465,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
void on_time_command_request(const TimeCommandRequest &msg) override; void on_time_command_request(const TimeCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_DATETIME
void on_date_time_command_request(const DateTimeCommandRequest &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

@@ -273,6 +273,15 @@ void APIServer::on_time_update(datetime::TimeEntity *obj) {
} }
#endif #endif
#ifdef USE_DATETIME_DATETIME
void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_datetime_state(obj);
}
#endif
#ifdef USE_TEXT #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

@@ -72,6 +72,9 @@ class APIServer : public Component, public Controller {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
void on_time_update(datetime::TimeEntity *obj) override; void on_time_update(datetime::TimeEntity *obj) override;
#endif #endif
#ifdef USE_DATETIME_DATETIME
void on_datetime_update(datetime::DateTimeEntity *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

@@ -71,6 +71,12 @@ bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { return this->cl
bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_info(time); } bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_info(time); }
#endif #endif
#ifdef USE_DATETIME_DATETIME
bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) {
return this->client_->send_datetime_info(datetime);
}
#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

@@ -52,6 +52,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool on_time(datetime::TimeEntity *time) override; bool on_time(datetime::TimeEntity *time) override;
#endif #endif
#ifdef USE_DATETIME_DATETIME
bool on_datetime(datetime::DateTimeEntity *datetime) 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

@@ -48,6 +48,11 @@ bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->cl
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); } bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); }
#endif #endif
#ifdef USE_DATETIME_DATETIME
bool InitialStateIterator::on_datetime(datetime::DateTimeEntity *datetime) {
return this->client_->send_datetime_state(datetime);
}
#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

@@ -49,6 +49,9 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool on_time(datetime::TimeEntity *time) override; bool on_time(datetime::TimeEntity *time) override;
#endif #endif
#ifdef USE_DATETIME_DATETIME
bool on_datetime(datetime::DateTimeEntity *datetime) 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

@@ -4,6 +4,7 @@ from esphome.components import uart
from esphome.const import CONF_ID, CONF_ADDRESS from esphome.const import CONF_ID, CONF_ADDRESS
CODEOWNERS = ["@s1lvi0"] CODEOWNERS = ["@s1lvi0"]
MULTI_CONF = True
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
CONF_BMS_DALY_ID = "bms_daly_id" CONF_BMS_DALY_ID = "bms_daly_id"

View File

@@ -1,6 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
# 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, time from esphome.components import mqtt, time
@@ -13,6 +12,7 @@ from esphome.const import (
CONF_TYPE, CONF_TYPE,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_DATE, CONF_DATE,
CONF_DATETIME,
CONF_TIME, CONF_TIME,
CONF_YEAR, CONF_YEAR,
CONF_MONTH, CONF_MONTH,
@@ -27,6 +27,7 @@ from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@rfdarter", "@jesserockz"] CODEOWNERS = ["@rfdarter", "@jesserockz"]
DEPENDENCIES = ["time"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@@ -34,10 +35,12 @@ 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) TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase)
DateTimeEntity = datetime_ns.class_("DateTimeEntity", DateTimeBase)
# Actions # Actions
DateSetAction = datetime_ns.class_("DateSetAction", automation.Action) DateSetAction = datetime_ns.class_("DateSetAction", automation.Action)
TimeSetAction = datetime_ns.class_("TimeSetAction", automation.Action) TimeSetAction = datetime_ns.class_("TimeSetAction", automation.Action)
DateTimeSetAction = datetime_ns.class_("DateTimeSetAction", automation.Action)
DateTimeStateTrigger = datetime_ns.class_( DateTimeStateTrigger = datetime_ns.class_(
"DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime) "DateTimeStateTrigger", automation.Trigger.template(cg.ESPTime)
@@ -46,6 +49,12 @@ DateTimeStateTrigger = datetime_ns.class_(
OnTimeTrigger = datetime_ns.class_( OnTimeTrigger = datetime_ns.class_(
"OnTimeTrigger", automation.Trigger, cg.Component, cg.Parented.template(TimeEntity) "OnTimeTrigger", automation.Trigger, cg.Component, cg.Parented.template(TimeEntity)
) )
OnDateTimeTrigger = datetime_ns.class_(
"OnDateTimeTrigger",
automation.Trigger,
cg.Component,
cg.Parented.template(DateTimeEntity),
)
DATETIME_MODES = [ DATETIME_MODES = [
"DATE", "DATE",
@@ -61,45 +70,55 @@ _DATETIME_SCHEMA = cv.Schema(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DateTimeStateTrigger),
} }
), ),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
} }
).extend(cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)) ).extend(cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA))
def date_schema(class_: MockObjClass) -> cv.Schema: def date_schema(class_: MockObjClass) -> cv.Schema:
schema = { schema = cv.Schema(
cv.GenerateID(): cv.declare_id(class_), {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDateComponent), cv.GenerateID(): cv.declare_id(class_),
cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDateComponent),
} cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True),
}
)
return _DATETIME_SCHEMA.extend(schema) return _DATETIME_SCHEMA.extend(schema)
def time_schema(class_: MockObjClass) -> cv.Schema: def time_schema(class_: MockObjClass) -> cv.Schema:
schema = { schema = cv.Schema(
cv.GenerateID(): cv.declare_id(class_), {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTimeComponent), cv.GenerateID(): cv.declare_id(class_),
cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTimeComponent),
cv.Inclusive( cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True),
CONF_ON_TIME, cv.Optional(CONF_ON_TIME): automation.validate_automation(
group_of_inclusion=CONF_ON_TIME, {
msg="`on_time` and `time_id` must both be specified", cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTimeTrigger),
): 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)
def datetime_schema(class_: MockObjClass) -> cv.Schema: def datetime_schema(class_: MockObjClass) -> cv.Schema:
schema = { schema = cv.Schema(
cv.GenerateID(): cv.declare_id(class_), {
cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of("DATETIME", upper=True), cv.GenerateID(): cv.declare_id(class_),
} cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTDateTimeComponent
),
cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of(
"DATETIME", upper=True
),
cv.Optional(CONF_ON_TIME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnDateTimeTrigger),
}
),
}
)
return _DATETIME_SCHEMA.extend(schema) return _DATETIME_SCHEMA.extend(schema)
@@ -113,13 +132,11 @@ async def setup_datetime_core_(var, config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) 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 = await cg.get_variable(config[CONF_TIME_ID])
rtc = None cg.add(var.set_rtc(rtc))
if rtc_id is not None:
rtc = await cg.get_variable(rtc_id)
for conf in config.get(CONF_ON_TIME, []): for conf in config.get(CONF_ON_TIME, []):
assert rtc is not None trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], rtc)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
await cg.register_component(trigger, conf) await cg.register_component(trigger, conf)
await cg.register_parented(trigger, var) await cg.register_parented(trigger, var)
@@ -161,16 +178,16 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args):
action_var = cg.new_Pvariable(action_id, template_arg) action_var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(action_var, config[CONF_ID]) await cg.register_parented(action_var, config[CONF_ID])
date = config[CONF_DATE] date_config = config[CONF_DATE]
if cg.is_template(date): if cg.is_template(date_config):
template_ = await cg.templatable(config[CONF_DATE], [], cg.ESPTime) template_ = await cg.templatable(date_config, [], cg.ESPTime)
cg.add(action_var.set_date(template_)) cg.add(action_var.set_date(template_))
else: else:
date_struct = cg.StructInitializer( date_struct = cg.StructInitializer(
cg.ESPTime, cg.ESPTime,
("day_of_month", date[CONF_DAY]), ("day_of_month", date_config[CONF_DAY]),
("month", date[CONF_MONTH]), ("month", date_config[CONF_MONTH]),
("year", date[CONF_YEAR]), ("year", date_config[CONF_YEAR]),
) )
cg.add(action_var.set_date(date_struct)) cg.add(action_var.set_date(date_struct))
return action_var return action_var
@@ -194,7 +211,7 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args):
time_config = config[CONF_TIME] time_config = config[CONF_TIME]
if cg.is_template(time_config): if cg.is_template(time_config):
template_ = await cg.templatable(config[CONF_TIME], [], cg.ESPTime) template_ = await cg.templatable(time_config, [], cg.ESPTime)
cg.add(action_var.set_time(template_)) cg.add(action_var.set_time(template_))
else: else:
time_struct = cg.StructInitializer( time_struct = cg.StructInitializer(
@@ -205,3 +222,35 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args):
) )
cg.add(action_var.set_time(time_struct)) cg.add(action_var.set_time(time_struct))
return action_var return action_var
@automation.register_action(
"datetime.datetime.set",
DateTimeSetAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(DateTimeEntity),
cv.Required(CONF_DATETIME): cv.Any(cv.returning_lambda, cv.date_time()),
},
),
)
async def datetime_datetime_set_to_code(config, action_id, template_arg, args):
action_var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(action_var, config[CONF_ID])
datetime_config = config[CONF_DATETIME]
if cg.is_template(datetime_config):
template_ = await cg.templatable(datetime_config, [], cg.ESPTime)
cg.add(action_var.set_datetime(template_))
else:
datetime_struct = cg.StructInitializer(
cg.ESPTime,
("second", datetime_config[CONF_SECOND]),
("minute", datetime_config[CONF_MINUTE]),
("hour", datetime_config[CONF_HOUR]),
("day_of_month", datetime_config[CONF_DAY]),
("month", datetime_config[CONF_MONTH]),
("year", datetime_config[CONF_YEAR]),
)
cg.add(action_var.set_datetime(datetime_struct))
return action_var

View File

@@ -40,10 +40,13 @@ void DateCall::validate_() {
if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) { if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) {
ESP_LOGE(TAG, "Year must be between 1970 and 3000"); ESP_LOGE(TAG, "Year must be between 1970 and 3000");
this->year_.reset(); this->year_.reset();
this->month_.reset();
this->day_.reset();
} }
if (this->month_.has_value() && (this->month_ < 1 || this->month_ > 12)) { if (this->month_.has_value() && (this->month_ < 1 || this->month_ > 12)) {
ESP_LOGE(TAG, "Month must be between 1 and 12"); ESP_LOGE(TAG, "Month must be between 1 and 12");
this->month_.reset(); this->month_.reset();
this->day_.reset();
} }
if (this->day_.has_value()) { if (this->day_.has_value()) {
uint16_t year = 0; uint16_t year = 0;

View File

@@ -5,6 +5,8 @@
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/time.h" #include "esphome/core/time.h"
#include "esphome/components/time/real_time_clock.h"
namespace esphome { namespace esphome {
namespace datetime { namespace datetime {
@@ -17,9 +19,14 @@ class DateTimeBase : public EntityBase {
void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); } void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; }
time::RealTimeClock *get_rtc() const { return this->rtc_; }
protected: protected:
CallbackManager<void()> state_callback_; CallbackManager<void()> state_callback_;
time::RealTimeClock *rtc_;
bool has_state_{false}; bool has_state_{false};
}; };

View File

@@ -0,0 +1,252 @@
#include "datetime_entity.h"
#ifdef USE_DATETIME_DATETIME
#include "esphome/core/log.h"
namespace esphome {
namespace datetime {
static const char *const TAG = "datetime.datetime_entity";
void DateTimeEntity::publish_state() {
if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) {
this->has_state_ = false;
return;
}
if (this->year_ < 1970 || this->year_ > 3000) {
this->has_state_ = false;
ESP_LOGE(TAG, "Year must be between 1970 and 3000");
return;
}
if (this->month_ < 1 || this->month_ > 12) {
this->has_state_ = false;
ESP_LOGE(TAG, "Month must be between 1 and 12");
return;
}
if (this->day_ > days_in_month(this->month_, this->year_)) {
this->has_state_ = false;
ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_);
return;
}
if (this->hour_ > 23) {
this->has_state_ = false;
ESP_LOGE(TAG, "Hour must be between 0 and 23");
return;
}
if (this->minute_ > 59) {
this->has_state_ = false;
ESP_LOGE(TAG, "Minute must be between 0 and 59");
return;
}
if (this->second_ > 59) {
this->has_state_ = false;
ESP_LOGE(TAG, "Second must be between 0 and 59");
return;
}
this->has_state_ = true;
ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_,
this->month_, this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call();
}
DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }
ESPTime DateTimeEntity::state_as_esptime() const {
ESPTime obj;
obj.year = this->year_;
obj.month = this->month_;
obj.day_of_month = this->day_;
obj.hour = this->hour_;
obj.minute = this->minute_;
obj.second = this->second_;
obj.day_of_week = 1; // Required to be valid for recalc_timestamp_local but not used.
obj.day_of_year = 1; // Required to be valid for recalc_timestamp_local but not used.
obj.recalc_timestamp_local(false);
return obj;
}
void DateTimeCall::validate_() {
if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) {
ESP_LOGE(TAG, "Year must be between 1970 and 3000");
this->year_.reset();
this->month_.reset();
this->day_.reset();
}
if (this->month_.has_value() && (this->month_ < 1 || this->month_ > 12)) {
ESP_LOGE(TAG, "Month must be between 1 and 12");
this->month_.reset();
this->day_.reset();
}
if (this->day_.has_value()) {
uint16_t year = 0;
uint8_t month = 0;
if (this->month_.has_value()) {
month = *this->month_;
} else {
if (this->parent_->month != 0) {
month = this->parent_->month;
} else {
ESP_LOGE(TAG, "Month must be set to validate day");
this->day_.reset();
}
}
if (this->year_.has_value()) {
year = *this->year_;
} else {
if (this->parent_->year != 0) {
year = this->parent_->year;
} else {
ESP_LOGE(TAG, "Year must be set to validate day");
this->day_.reset();
}
}
if (this->day_.has_value() && *this->day_ > days_in_month(month, year)) {
ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(month, year), month);
this->day_.reset();
}
}
if (this->hour_.has_value() && this->hour_ > 23) {
ESP_LOGE(TAG, "Hour must be between 0 and 23");
this->hour_.reset();
}
if (this->minute_.has_value() && this->minute_ > 59) {
ESP_LOGE(TAG, "Minute must be between 0 and 59");
this->minute_.reset();
}
if (this->second_.has_value() && this->second_ > 59) {
ESP_LOGE(TAG, "Second must be between 0 and 59");
this->second_.reset();
}
}
void DateTimeCall::perform() {
this->validate_();
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
if (this->year_.has_value()) {
ESP_LOGD(TAG, " Year: %d", *this->year_);
}
if (this->month_.has_value()) {
ESP_LOGD(TAG, " Month: %d", *this->month_);
}
if (this->day_.has_value()) {
ESP_LOGD(TAG, " Day: %d", *this->day_);
}
if (this->hour_.has_value()) {
ESP_LOGD(TAG, " Hour: %d", *this->hour_);
}
if (this->minute_.has_value()) {
ESP_LOGD(TAG, " Minute: %d", *this->minute_);
}
if (this->second_.has_value()) {
ESP_LOGD(TAG, " Second: %d", *this->second_);
}
this->parent_->control(*this);
}
DateTimeCall &DateTimeCall::set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
uint8_t second) {
this->year_ = year;
this->month_ = month;
this->day_ = day;
this->hour_ = hour;
this->minute_ = minute;
this->second_ = second;
return *this;
};
DateTimeCall &DateTimeCall::set_datetime(ESPTime datetime) {
return this->set_datetime(datetime.year, datetime.month, datetime.day_of_month, datetime.hour, datetime.minute,
datetime.second);
};
DateTimeCall &DateTimeCall::set_datetime(const std::string &datetime) {
ESPTime val{};
if (!ESPTime::strptime(datetime, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this;
}
return this->set_datetime(val);
}
DateTimeCall &DateTimeCall::set_datetime(time_t epoch_seconds) {
ESPTime val = ESPTime::from_epoch_local(epoch_seconds);
return this->set_datetime(val);
}
DateTimeCall DateTimeEntityRestoreState::to_call(DateTimeEntity *datetime) {
DateTimeCall call = datetime->make_call();
call.set_datetime(this->year, this->month, this->day, this->hour, this->minute, this->second);
return call;
}
void DateTimeEntityRestoreState::apply(DateTimeEntity *time) {
time->year_ = this->year;
time->month_ = this->month;
time->day_ = this->day;
time->hour_ = this->hour;
time->minute_ = this->minute;
time->second_ = this->second;
time->publish_state();
}
static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider
// there has been a drastic time synchronization
void OnDateTimeTrigger::loop() {
if (!this->parent_->has_state()) {
return;
}
ESPTime time = this->parent_->rtc_->now();
if (!time.is_valid()) {
return;
}
if (this->last_check_.has_value()) {
if (*this->last_check_ > time && this->last_check_->timestamp - time.timestamp > MAX_TIMESTAMP_DRIFT) {
// We went back in time (a lot), probably caused by time synchronization
ESP_LOGW(TAG, "Time has jumped back!");
} else if (*this->last_check_ >= time) {
// already handled this one
return;
} else if (time > *this->last_check_ && time.timestamp - this->last_check_->timestamp > MAX_TIMESTAMP_DRIFT) {
// We went ahead in time (a lot), probably caused by time synchronization
ESP_LOGW(TAG, "Time has jumped ahead!");
this->last_check_ = time;
return;
}
while (true) {
this->last_check_->increment_second();
if (*this->last_check_ >= time)
break;
if (this->matches_(*this->last_check_)) {
this->trigger();
break;
}
}
}
this->last_check_ = time;
if (!time.fields_in_range()) {
ESP_LOGW(TAG, "Time is out of range!");
ESP_LOGD(TAG, "Second=%02u Minute=%02u Hour=%02u Day=%02u Month=%02u Year=%04u", time.second, time.minute,
time.hour, time.day_of_month, time.month, time.year);
}
if (this->matches_(time))
this->trigger();
}
bool OnDateTimeTrigger::matches_(const ESPTime &time) const {
return time.is_valid() && time.year == this->parent_->year && time.month == this->parent_->month &&
time.day_of_month == this->parent_->day && time.hour == this->parent_->hour &&
time.minute == this->parent_->minute && time.second == this->parent_->second;
}
} // namespace datetime
} // namespace esphome
#endif // USE_DATETIME_TIME

View File

@@ -0,0 +1,150 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_DATETIME_DATETIME
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "esphome/core/time.h"
#include "datetime_base.h"
namespace esphome {
namespace datetime {
#define LOG_DATETIME_DATETIME(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
if (!(obj)->get_icon().empty()) { \
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \
} \
}
class DateTimeCall;
class DateTimeEntity;
struct DateTimeEntityRestoreState {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
DateTimeCall to_call(DateTimeEntity *datetime);
void apply(DateTimeEntity *datetime);
} __attribute__((packed));
class DateTimeEntity : public DateTimeBase {
protected:
uint16_t year_;
uint8_t month_;
uint8_t day_;
uint8_t hour_;
uint8_t minute_;
uint8_t second_;
public:
void publish_state();
DateTimeCall make_call();
ESPTime state_as_esptime() const override;
const uint16_t &year = year_;
const uint8_t &month = month_;
const uint8_t &day = day_;
const uint8_t &hour = hour_;
const uint8_t &minute = minute_;
const uint8_t &second = second_;
protected:
friend class DateTimeCall;
friend struct DateTimeEntityRestoreState;
friend class OnDateTimeTrigger;
virtual void control(const DateTimeCall &call) = 0;
};
class DateTimeCall {
public:
explicit DateTimeCall(DateTimeEntity *parent) : parent_(parent) {}
void perform();
DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
DateTimeCall &set_datetime(ESPTime datetime);
DateTimeCall &set_datetime(const std::string &datetime);
DateTimeCall &set_datetime(time_t epoch_seconds);
DateTimeCall &set_year(uint16_t year) {
this->year_ = year;
return *this;
}
DateTimeCall &set_month(uint8_t month) {
this->month_ = month;
return *this;
}
DateTimeCall &set_day(uint8_t day) {
this->day_ = day;
return *this;
}
DateTimeCall &set_hour(uint8_t hour) {
this->hour_ = hour;
return *this;
}
DateTimeCall &set_minute(uint8_t minute) {
this->minute_ = minute;
return *this;
}
DateTimeCall &set_second(uint8_t second) {
this->second_ = second;
return *this;
}
optional<uint16_t> get_year() const { return this->year_; }
optional<uint8_t> get_month() const { return this->month_; }
optional<uint8_t> get_day() const { return this->day_; }
optional<uint8_t> get_hour() const { return this->hour_; }
optional<uint8_t> get_minute() const { return this->minute_; }
optional<uint8_t> get_second() const { return this->second_; }
protected:
void validate_();
DateTimeEntity *parent_;
optional<uint16_t> year_;
optional<uint8_t> month_;
optional<uint8_t> day_;
optional<uint8_t> hour_;
optional<uint8_t> minute_;
optional<uint8_t> second_;
};
template<typename... Ts> class DateTimeSetAction : public Action<Ts...>, public Parented<DateTimeEntity> {
public:
TEMPLATABLE_VALUE(ESPTime, datetime)
void play(Ts... x) override {
auto call = this->parent_->make_call();
if (this->datetime_.has_value()) {
call.set_datetime(this->datetime_.value(x...));
}
call.perform();
}
};
class OnDateTimeTrigger : public Trigger<>, public Component, public Parented<DateTimeEntity> {
public:
void loop() override;
protected:
bool matches_(const ESPTime &time) const;
optional<ESPTime> last_check_;
};
} // namespace datetime
} // namespace esphome
#endif // USE_DATETIME_DATETIME

View File

@@ -94,8 +94,6 @@ void TimeEntityRestoreState::apply(TimeEntity *time) {
time->publish_state(); time->publish_state();
} }
#ifdef USE_TIME
static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider
// there has been a drastic time synchronization // there has been a drastic time synchronization
@@ -103,7 +101,7 @@ void OnTimeTrigger::loop() {
if (!this->parent_->has_state()) { if (!this->parent_->has_state()) {
return; return;
} }
ESPTime time = this->rtc_->now(); ESPTime time = this->parent_->rtc_->now();
if (!time.is_valid()) { if (!time.is_valid()) {
return; return;
} }
@@ -148,8 +146,6 @@ bool OnTimeTrigger::matches_(const ESPTime &time) const {
time.second == this->parent_->second; time.second == this->parent_->second;
} }
#endif
} // namespace datetime } // namespace datetime
} // namespace esphome } // namespace esphome

View File

@@ -10,10 +10,6 @@
#include "datetime_base.h" #include "datetime_base.h"
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
namespace esphome { namespace esphome {
namespace datetime { namespace datetime {
@@ -27,6 +23,7 @@ namespace datetime {
class TimeCall; class TimeCall;
class TimeEntity; class TimeEntity;
class OnTimeTrigger;
struct TimeEntityRestoreState { struct TimeEntityRestoreState {
uint8_t hour; uint8_t hour;
@@ -62,6 +59,7 @@ class TimeEntity : public DateTimeBase {
protected: protected:
friend class TimeCall; friend class TimeCall;
friend struct TimeEntityRestoreState; friend struct TimeEntityRestoreState;
friend class OnTimeTrigger;
virtual void control(const TimeCall &call) = 0; virtual void control(const TimeCall &call) = 0;
}; };
@@ -115,22 +113,16 @@ template<typename... Ts> class TimeSetAction : public Action<Ts...>, public Pare
} }
}; };
#ifdef USE_TIME
class OnTimeTrigger : public Trigger<>, public Component, public Parented<TimeEntity> { class OnTimeTrigger : public Trigger<>, public Component, public Parented<TimeEntity> {
public: public:
explicit OnTimeTrigger(time::RealTimeClock *rtc) : rtc_(rtc) {}
void loop() override; void loop() override;
protected: protected:
bool matches_(const ESPTime &time) const; bool matches_(const ESPTime &time) const;
time::RealTimeClock *rtc_;
optional<ESPTime> last_check_; optional<ESPTime> last_check_;
}; };
#endif
} // namespace datetime } // namespace datetime
} // namespace esphome } // namespace esphome

View File

@@ -129,7 +129,13 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
uint8_t bitmask = 0; uint8_t bitmask = 0;
uint8_t pixel_data = 0; uint8_t pixel_data = 0;
float bpp_max = (1 << this->bpp_) - 1; uint8_t bpp_max = (1 << this->bpp_) - 1;
auto diff_r = (float) color.r - (float) background.r;
auto diff_g = (float) color.g - (float) background.g;
auto diff_b = (float) color.b - (float) background.b;
auto b_r = (float) background.r;
auto b_g = (float) background.g;
auto b_b = (float) background.g;
for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) { for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) {
for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) { for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) {
uint8_t pixel = 0; uint8_t pixel = 0;
@@ -146,12 +152,9 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
if (pixel == bpp_max) { if (pixel == bpp_max) {
display->draw_pixel_at(glyph_x, glyph_y, color); display->draw_pixel_at(glyph_x, glyph_y, color);
} else if (pixel != 0) { } else if (pixel != 0) {
float on = (float) pixel / bpp_max; auto on = (float) pixel / (float) bpp_max;
float off = 1.0 - on; auto blended =
Color blended; Color((uint8_t) (diff_r * on + b_r), (uint8_t) (diff_g * on + b_g), (uint8_t) (diff_b * on + b_b));
blended.r = color.r * on + background.r * off;
blended.g = color.r * on + background.g * off;
blended.b = color.r * on + background.b * off;
display->draw_pixel_at(glyph_x, glyph_y, blended); display->draw_pixel_at(glyph_x, glyph_y, blended);
} }
} }

View File

@@ -164,7 +164,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
ESP_LOGV(TAG, "Updating graph. ymin %f, ymax %f", ymin, ymax); ESP_LOGV(TAG, "Updating graph. ymin %f, ymax %f", ymin, ymax);
for (auto *trace : traces_) { for (auto *trace : traces_) {
Color c = trace->get_line_color(); Color c = trace->get_line_color();
uint16_t thick = trace->get_line_thickness(); int16_t thick = trace->get_line_thickness();
bool continuous = trace->get_continuous(); bool continuous = trace->get_continuous();
bool has_prev = false; bool has_prev = false;
bool prev_b = false; bool prev_b = false;
@@ -178,20 +178,20 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
if (b) { if (b) {
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset; int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) { if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {
for (uint16_t t = 0; t < thick; t++) { for (int16_t t = 0; t < thick; t++) {
buff->draw_pixel_at(x, y + t, c); buff->draw_pixel_at(x, y + t, c);
} }
} else { } else {
int16_t mid_y = (y + prev_y + thick) / 2; int16_t mid_y = (y + prev_y + thick) / 2;
if (y > prev_y) { if (y > prev_y) {
for (uint16_t t = prev_y + thick; t <= mid_y; t++) for (int16_t t = prev_y + thick; t <= mid_y; t++)
buff->draw_pixel_at(x + 1, t, c); buff->draw_pixel_at(x + 1, t, c);
for (uint16_t t = mid_y + 1; t < y + thick; t++) for (int16_t t = mid_y + 1; t < y + thick; t++)
buff->draw_pixel_at(x, t, c); buff->draw_pixel_at(x, t, c);
} else { } else {
for (uint16_t t = prev_y - 1; t >= mid_y; t--) for (int16_t t = prev_y - 1; t >= mid_y; t--)
buff->draw_pixel_at(x + 1, t, c); buff->draw_pixel_at(x + 1, t, c);
for (uint16_t t = mid_y - 1; t >= y; t--) for (int16_t t = mid_y - 1; t >= y; t--)
buff->draw_pixel_at(x, t, c); buff->draw_pixel_at(x, t, c);
} }
} }

View File

@@ -104,7 +104,8 @@ void GraphicalDisplayMenu::draw(display::Display *display, const display::Rect *
} }
void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display::Rect *bounds) { void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display::Rect *bounds) {
int total_height = 0; int16_t total_height = 0;
int16_t max_width = 0;
int y_padding = 2; int y_padding = 2;
bool scroll_menu_items = false; bool scroll_menu_items = false;
std::vector<display::Rect> menu_dimensions; std::vector<display::Rect> menu_dimensions;
@@ -118,6 +119,7 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const
menu_dimensions.push_back(item_dimensions); menu_dimensions.push_back(item_dimensions);
total_height += item_dimensions.h + (i == 0 ? 0 : y_padding); total_height += item_dimensions.h + (i == 0 ? 0 : y_padding);
max_width = std::max(max_width, item_dimensions.w);
if (total_height <= bounds->h) { if (total_height <= bounds->h) {
number_items_fit_to_screen++; number_items_fit_to_screen++;
@@ -166,7 +168,8 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const
// Render the items into the view port // Render the items into the view port
display->start_clipping(*bounds); display->start_clipping(*bounds);
int y_offset = bounds->y; display->filled_rectangle(bounds->x, bounds->y, max_width, total_height, this->background_color_);
auto y_offset = bounds->y;
for (size_t i = first_item_index; i <= last_item_index; i++) { for (size_t i = first_item_index; i <= last_item_index; i++) {
const auto *item = this->displayed_item_->get_item(i); const auto *item = this->displayed_item_->get_item(i);
const bool selected = i == this->cursor_index_; const bool selected = i == this->cursor_index_;
@@ -176,7 +179,7 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const
dimensions.x = bounds->x; dimensions.x = bounds->x;
this->draw_item(display, item, &dimensions, selected); this->draw_item(display, item, &dimensions, selected);
y_offset = dimensions.y + dimensions.h + y_padding; y_offset += dimensions.h + y_padding;
} }
display->end_clipping(); display->end_clipping();
@@ -219,9 +222,7 @@ inline void GraphicalDisplayMenu::draw_item(display::Display *display, const dis
// int background_width = std::max(bounds->width, available_width); // int background_width = std::max(bounds->width, available_width);
int background_width = bounds->w; int background_width = bounds->w;
if (selected) { display->filled_rectangle(bounds->x, bounds->y, background_width, bounds->h, background_color);
display->filled_rectangle(bounds->x, bounds->y, background_width, bounds->h, background_color);
}
std::string label = item->get_text(); std::string label = item->get_text();
if (item->has_value()) { if (item->has_value()) {
@@ -230,7 +231,7 @@ inline void GraphicalDisplayMenu::draw_item(display::Display *display, const dis
} }
display->print(bounds->x, bounds->y, this->font_, foreground_color, display::TextAlign::TOP_LEFT, label.c_str(), display->print(bounds->x, bounds->y, this->font_, foreground_color, display::TextAlign::TOP_LEFT, label.c_str(),
~foreground_color); background_color);
} }
void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) { void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) {

View File

@@ -115,6 +115,7 @@ MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent)
MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent) MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent)
MQTTDateComponent = mqtt_ns.class_("MQTTDateComponent", MQTTComponent) MQTTDateComponent = mqtt_ns.class_("MQTTDateComponent", MQTTComponent)
MQTTTimeComponent = mqtt_ns.class_("MQTTTimeComponent", MQTTComponent) MQTTTimeComponent = mqtt_ns.class_("MQTTTimeComponent", MQTTComponent)
MQTTDateTimeComponent = mqtt_ns.class_("MQTTDateTimeComponent", 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

@@ -0,0 +1,84 @@
#include "mqtt_datetime.h"
#include <utility>
#include "esphome/core/log.h"
#include "mqtt_const.h"
#ifdef USE_MQTT
#ifdef USE_DATETIME_TIME
namespace esphome {
namespace mqtt {
static const char *const TAG = "mqtt.datetime.time";
using namespace esphome::datetime;
MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetime_(datetime) {}
void MQTTDateTimeComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
auto call = this->datetime_->make_call();
if (root.containsKey("year")) {
call.set_year(root["year"]);
}
if (root.containsKey("month")) {
call.set_month(root["month"]);
}
if (root.containsKey("day")) {
call.set_day(root["day"]);
}
if (root.containsKey("hour")) {
call.set_hour(root["hour"]);
}
if (root.containsKey("minute")) {
call.set_minute(root["minute"]);
}
if (root.containsKey("second")) {
call.set_second(root["second"]);
}
call.perform();
});
this->datetime_->add_on_state_callback([this]() {
this->publish_state(this->datetime_->year, this->datetime_->month, this->datetime_->day, this->datetime_->hour,
this->datetime_->minute, this->datetime_->second);
});
}
void MQTTDateTimeComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT DateTime '%s':", this->datetime_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
}
std::string MQTTDateTimeComponent::component_type() const { return "datetime"; }
const EntityBase *MQTTDateTimeComponent::get_entity() const { return this->datetime_; }
void MQTTDateTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// Nothing extra to add here
}
bool MQTTDateTimeComponent::send_initial_state() {
if (this->datetime_->has_state()) {
return this->publish_state(this->datetime_->year, this->datetime_->month, this->datetime_->day,
this->datetime_->hour, this->datetime_->minute, this->datetime_->second);
} else {
return true;
}
}
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
uint8_t second) {
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
root["year"] = year;
root["month"] = month;
root["day"] = day;
root["hour"] = hour;
root["minute"] = minute;
root["second"] = second;
});
}
} // namespace mqtt
} // namespace esphome
#endif // USE_DATETIME_TIME
#endif // USE_MQTT

View File

@@ -0,0 +1,45 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_MQTT
#ifdef USE_DATETIME_TIME
#include "esphome/components/datetime/datetime_entity.h"
#include "mqtt_component.h"
namespace esphome {
namespace mqtt {
class MQTTDateTimeComponent : public mqtt::MQTTComponent {
public:
/** Construct this MQTTDateTimeComponent instance with the provided friendly_name and time
*
* @param time The time entity.
*/
explicit MQTTDateTimeComponent(datetime::DateTimeEntity *time);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Override setup.
void setup() override;
void dump_config() override;
void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override;
bool send_initial_state() override;
bool publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
protected:
std::string component_type() const override;
const EntityBase *get_entity() const override;
datetime::DateTimeEntity *datetime_;
};
} // namespace mqtt
} // namespace esphome
#endif // USE_DATETIME_DATE
#endif // USE_MQTT

View File

@@ -31,6 +31,10 @@ TemplateTime = template_ns.class_(
"TemplateTime", datetime.TimeEntity, cg.PollingComponent "TemplateTime", datetime.TimeEntity, cg.PollingComponent
) )
TemplateDateTime = template_ns.class_(
"TemplateDateTime", datetime.DateTimeEntity, cg.PollingComponent
)
def validate(config): def validate(config):
config = config.copy() config = config.copy()
@@ -78,6 +82,13 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_INITIAL_VALUE): cv.date_time(allowed_date=False), cv.Optional(CONF_INITIAL_VALUE): cv.date_time(allowed_date=False),
} }
), ),
"DATETIME": datetime.datetime_schema(TemplateDateTime)
.extend(_BASE_SCHEMA)
.extend(
{
cv.Optional(CONF_INITIAL_VALUE): cv.date_time(),
}
),
}, },
upper=True, upper=True,
), ),
@@ -116,6 +127,17 @@ async def to_code(config):
("hour", initial_value[CONF_HOUR]), ("hour", initial_value[CONF_HOUR]),
) )
cg.add(var.set_initial_value(time_struct)) cg.add(var.set_initial_value(time_struct))
elif config[CONF_TYPE] == "DATETIME":
datetime_struct = cg.StructInitializer(
cg.ESPTime,
("second", initial_value[CONF_SECOND]),
("minute", initial_value[CONF_MINUTE]),
("hour", initial_value[CONF_HOUR]),
("day_of_month", initial_value[CONF_DAY]),
("month", initial_value[CONF_MONTH]),
("year", initial_value[CONF_YEAR]),
)
cg.add(var.set_initial_value(datetime_struct))
if CONF_SET_ACTION in config: if CONF_SET_ACTION in config:
await automation.build_automation( await automation.build_automation(

View File

@@ -0,0 +1,150 @@
#include "template_datetime.h"
#ifdef USE_DATETIME_DATETIME
#include "esphome/core/log.h"
namespace esphome {
namespace template_ {
static const char *const TAG = "template.datetime";
void TemplateDateTime::setup() {
if (this->f_.has_value())
return;
ESPTime state{};
if (!this->restore_value_) {
state = this->initial_value_;
} else {
datetime::DateTimeEntityRestoreState temp;
this->pref_ = global_preferences->make_preference<datetime::DateTimeEntityRestoreState>(194434090U ^
this->get_object_id_hash());
if (this->pref_.load(&temp)) {
temp.apply(this);
return;
} else {
// set to inital value if loading from pref failed
state = this->initial_value_;
}
}
this->year_ = state.year;
this->month_ = state.month;
this->day_ = state.day_of_month;
this->hour_ = state.hour;
this->minute_ = state.minute;
this->second_ = state.second;
this->publish_state();
}
void TemplateDateTime::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->year_ = val->year;
this->month_ = val->month;
this->day_ = val->day_of_month;
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
}
void TemplateDateTime::control(const datetime::DateTimeCall &call) {
bool has_year = call.get_year().has_value();
bool has_month = call.get_month().has_value();
bool has_day = call.get_day().has_value();
bool has_hour = call.get_hour().has_value();
bool has_minute = call.get_minute().has_value();
bool has_second = call.get_second().has_value();
ESPTime value = {};
if (has_year)
value.year = *call.get_year();
if (has_month)
value.month = *call.get_month();
if (has_day)
value.day_of_month = *call.get_day();
if (has_hour)
value.hour = *call.get_hour();
if (has_minute)
value.minute = *call.get_minute();
if (has_second)
value.second = *call.get_second();
this->set_trigger_->trigger(value);
if (this->optimistic_) {
if (has_year)
this->year_ = *call.get_year();
if (has_month)
this->month_ = *call.get_month();
if (has_day)
this->day_ = *call.get_day();
if (has_hour)
this->hour_ = *call.get_hour();
if (has_minute)
this->minute_ = *call.get_minute();
if (has_second)
this->second_ = *call.get_second();
this->publish_state();
}
if (this->restore_value_) {
datetime::DateTimeEntityRestoreState temp = {};
if (has_year) {
temp.year = *call.get_year();
} else {
temp.year = this->year_;
}
if (has_month) {
temp.month = *call.get_month();
} else {
temp.month = this->month_;
}
if (has_day) {
temp.day = *call.get_day();
} else {
temp.day = this->day_;
}
if (has_hour) {
temp.hour = *call.get_hour();
} else {
temp.hour = this->hour_;
}
if (has_minute) {
temp.minute = *call.get_minute();
} else {
temp.minute = this->minute_;
}
if (has_second) {
temp.second = *call.get_second();
} else {
temp.second = this->second_;
}
this->pref_.save(&temp);
}
}
void TemplateDateTime::dump_config() {
LOG_DATETIME_DATETIME("", "Template DateTime", this);
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
LOG_UPDATE_INTERVAL(this);
}
} // namespace template_
} // namespace esphome
#endif // USE_DATETIME_DATETIME

View File

@@ -0,0 +1,46 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_DATETIME_TIME
#include "esphome/components/datetime/datetime_entity.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/core/time.h"
namespace esphome {
namespace template_ {
class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent {
public:
void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; }
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
Trigger<ESPTime> *get_set_trigger() const { return this->set_trigger_; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; }
void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
protected:
void control(const datetime::DateTimeCall &call) override;
bool optimistic_{false};
ESPTime initial_value_{};
bool restore_value_{false};
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
optional<std::function<optional<ESPTime>()>> f_;
ESPPreferenceObject pref_;
};
} // namespace template_
} // namespace esphome
#endif // USE_DATETIME_TIME

View File

@@ -13,6 +13,8 @@
#endif #endif
#include <cerrno> #include <cerrno>
#include <cinttypes>
namespace esphome { namespace esphome {
namespace time { namespace time {

View File

@@ -23,6 +23,7 @@ class TimeBasedCover : public cover::Cover, public Component {
void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; } void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; }
void set_manual_control(bool value) { this->manual_control_ = value; } void set_manual_control(bool value) { this->manual_control_ = value; }
void set_assumed_state(bool value) { this->assumed_state_ = value; } void set_assumed_state(bool value) { this->assumed_state_ = value; }
cover::CoverOperation get_last_operation() const { return this->last_operation_; }
protected: protected:
void control(const cover::CoverCall &call) override; void control(const cover::CoverCall &call) override;

View File

@@ -129,6 +129,15 @@ bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) {
} }
#endif #endif
#ifdef USE_DATETIME_DATETIME
bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) {
if (this->web_server_->events_.count() == 0)
return true;
this->web_server_->events_.send(this->web_server_->datetime_json(datetime, DETAIL_ALL).c_str(), "state");
return true;
}
#endif
#ifdef USE_TEXT #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

@@ -47,6 +47,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool on_time(datetime::TimeEntity *time) override; bool on_time(datetime::TimeEntity *time) override;
#endif #endif
#ifdef USE_DATETIME_DATETIME
bool on_datetime(datetime::DateTimeEntity *datetime) 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

@@ -926,6 +926,8 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
void WebServer::on_time_update(datetime::TimeEntity *obj) { void WebServer::on_time_update(datetime::TimeEntity *obj) {
if (this->events_.count() == 0)
return;
this->events_.send(this->time_json(obj, DETAIL_STATE).c_str(), "state"); this->events_.send(this->time_json(obj, DETAIL_STATE).c_str(), "state");
} }
void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -970,6 +972,55 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con
} }
#endif // USE_DATETIME_TIME #endif // USE_DATETIME_TIME
#ifdef USE_DATETIME_DATETIME
void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) {
if (this->events_.count() == 0)
return;
this->events_.send(this->datetime_json(obj, DETAIL_STATE).c_str(), "state");
}
void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_datetimes()) {
if (obj->get_object_id() != match.id)
continue;
if (request->method() == HTTP_GET && match.method.empty()) {
std::string data = this->datetime_json(obj, DETAIL_STATE);
request->send(200, "application/json", data.c_str());
return;
}
if (match.method != "set") {
request->send(404);
return;
}
auto call = obj->make_call();
if (!request->hasParam("value")) {
request->send(409);
return;
}
if (request->hasParam("value")) {
std::string value = request->getParam("value")->value().c_str();
call.set_datetime(value);
}
this->schedule_([call]() mutable { call.perform(); });
request->send(200);
return;
}
request->send(404);
}
std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) {
return json::build_json([obj, start_config](JsonObject root) {
set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config);
std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour,
obj->minute, obj->second);
root["value"] = value;
root["state"] = value;
});
}
#endif // USE_DATETIME_DATETIME
#ifdef USE_TEXT #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)
@@ -1458,6 +1509,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
return true; return true;
#endif #endif
#ifdef USE_DATETIME_DATETIME
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "datetime")
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;
@@ -1595,6 +1651,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
} }
#endif #endif
#ifdef USE_DATETIME_DATETIME
if (match.domain == "datetime") {
this->handle_datetime_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

@@ -239,6 +239,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
std::string time_json(datetime::TimeEntity *obj, JsonDetail start_config); std::string time_json(datetime::TimeEntity *obj, JsonDetail start_config);
#endif #endif
#ifdef USE_DATETIME_DATETIME
void on_datetime_update(datetime::DateTimeEntity *obj) override;
/// Handle a datetime request under '/datetime/<id>'.
void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match);
/// Dump the datetime state with its value as a JSON string.
std::string datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config);
#endif
#ifdef USE_TEXT #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

@@ -181,6 +181,7 @@ CONF_DATA_PINS = "data_pins"
CONF_DATA_RATE = "data_rate" CONF_DATA_RATE = "data_rate"
CONF_DATA_TEMPLATE = "data_template" CONF_DATA_TEMPLATE = "data_template"
CONF_DATE = "date" CONF_DATE = "date"
CONF_DATETIME = "datetime"
CONF_DAY = "day" CONF_DAY = "day"
CONF_DAYS_OF_MONTH = "days_of_month" CONF_DAYS_OF_MONTH = "days_of_month"
CONF_DAYS_OF_WEEK = "days_of_week" CONF_DAYS_OF_WEEK = "days_of_week"

View File

@@ -45,6 +45,9 @@
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
#include "esphome/components/datetime/time_entity.h" #include "esphome/components/datetime/time_entity.h"
#endif #endif
#ifdef USE_DATETIME_DATETIME
#include "esphome/components/datetime/datetime_entity.h"
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
#include "esphome/components/text/text.h" #include "esphome/components/text/text.h"
#endif #endif
@@ -141,6 +144,10 @@ class Application {
void register_time(datetime::TimeEntity *time) { this->times_.push_back(time); } void register_time(datetime::TimeEntity *time) { this->times_.push_back(time); }
#endif #endif
#ifdef USE_DATETIME_DATETIME
void register_datetime(datetime::DateTimeEntity *datetime) { this->datetimes_.push_back(datetime); }
#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
@@ -335,6 +342,15 @@ class Application {
return nullptr; return nullptr;
} }
#endif #endif
#ifdef USE_DATETIME_DATETIME
const std::vector<datetime::DateTimeEntity *> &get_datetimes() { return this->datetimes_; }
datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) {
for (auto *obj : this->datetimes_)
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
return obj;
return nullptr;
}
#endif
#ifdef USE_TEXT #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) {
@@ -456,6 +472,9 @@ class Application {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
std::vector<datetime::TimeEntity *> times_{}; std::vector<datetime::TimeEntity *> times_{};
#endif #endif
#ifdef USE_DATETIME_DATETIME
std::vector<datetime::DateTimeEntity *> datetimes_{};
#endif
#ifdef USE_SELECT #ifdef USE_SELECT
std::vector<select::Select *> selects_{}; std::vector<select::Select *> selects_{};
#endif #endif

View File

@@ -232,6 +232,21 @@ void ComponentIterator::advance() {
} }
break; break;
#endif #endif
#ifdef USE_DATETIME_DATETIME
case IteratorState::DATETIME_DATETIME:
if (this->at_ >= App.get_datetimes().size()) {
advance_platform = true;
} else {
auto *datetime = App.get_datetimes()[this->at_];
if (datetime->is_internal() && !this->include_internal_) {
success = true;
break;
} else {
success = this->on_datetime(datetime);
}
}
break;
#endif
#ifdef USE_TEXT #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

@@ -63,6 +63,9 @@ class ComponentIterator {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
virtual bool on_time(datetime::TimeEntity *time) = 0; virtual bool on_time(datetime::TimeEntity *time) = 0;
#endif #endif
#ifdef USE_DATETIME_DATETIME
virtual bool on_datetime(datetime::DateTimeEntity *datetime) = 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
@@ -132,6 +135,9 @@ class ComponentIterator {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
DATETIME_TIME, DATETIME_TIME,
#endif #endif
#ifdef USE_DATETIME_DATETIME
DATETIME_DATETIME,
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
TEXT, TEXT,
#endif #endif

View File

@@ -71,6 +71,12 @@ void Controller::setup_controller(bool include_internal) {
obj->add_on_state_callback([this, obj]() { this->on_time_update(obj); }); obj->add_on_state_callback([this, obj]() { this->on_time_update(obj); });
} }
#endif #endif
#ifdef USE_DATETIME_DATETIME
for (auto *obj : App.get_datetimes()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_datetime_update(obj); });
}
#endif
#ifdef USE_TEXT #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

@@ -37,6 +37,9 @@
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
#include "esphome/components/datetime/time_entity.h" #include "esphome/components/datetime/time_entity.h"
#endif #endif
#ifdef USE_DATETIME_DATETIME
#include "esphome/components/datetime/datetime_entity.h"
#endif
#ifdef USE_TEXT #ifdef USE_TEXT
#include "esphome/components/text/text.h" #include "esphome/components/text/text.h"
#endif #endif
@@ -97,6 +100,9 @@ class Controller {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
virtual void on_time_update(datetime::TimeEntity *obj){}; virtual void on_time_update(datetime::TimeEntity *obj){};
#endif #endif
#ifdef USE_DATETIME_DATETIME
virtual void on_datetime_update(datetime::DateTimeEntity *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

@@ -39,6 +39,7 @@
#define USE_DATETIME #define USE_DATETIME
#define USE_DATETIME_DATE #define USE_DATETIME_DATE
#define USE_DATETIME_TIME #define USE_DATETIME_TIME
#define USE_DATETIME_DATETIME
#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

@@ -178,6 +178,15 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
this->timestamp = res; this->timestamp = res;
} }
void ESPTime::recalc_timestamp_local(bool use_day_of_year) {
this->recalc_timestamp_utc(use_day_of_year);
this->timestamp -= ESPTime::timezone_offset();
ESPTime temp = ESPTime::from_epoch_local(this->timestamp);
if (temp.is_dst) {
this->timestamp -= 3600;
}
}
int32_t ESPTime::timezone_offset() { int32_t ESPTime::timezone_offset() {
int32_t offset = 0; int32_t offset = 0;
time_t now = ::time(nullptr); time_t now = ::time(nullptr);

View File

@@ -99,6 +99,9 @@ struct ESPTime {
/// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC). /// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC).
void recalc_timestamp_utc(bool use_day_of_year = true); void recalc_timestamp_utc(bool use_day_of_year = true);
/// Recalculate the timestamp field from the other fields of this ESPTime instance assuming local fields.
void recalc_timestamp_local(bool use_day_of_year = true);
/// Convert this ESPTime instance back to a tm struct. /// Convert this ESPTime instance back to a tm struct.
struct tm to_c_tm(); struct tm to_c_tm();

View File

@@ -624,6 +624,7 @@ def lint_trailing_whitespace(fname, match):
"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/datetime/time_entity.h",
"esphome/components/datetime/datetime_entity.h",
"esphome/components/display/display.h", "esphome/components/display/display.h",
"esphome/components/event/event.h", "esphome/components/event/event.h",
"esphome/components/fan/fan.h", "esphome/components/fan/fan.h",

View File

@@ -6,9 +6,12 @@ import re
from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.button import ButtonDeviceClass from homeassistant.components.button import ButtonDeviceClass
from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.cover import CoverDeviceClass
from homeassistant.components.event import EventDeviceClass
from homeassistant.components.number import NumberDeviceClass from homeassistant.components.number import NumberDeviceClass
from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.components.text_sensor import TextSensorDeviceClass
from homeassistant.components.valve import ValveDeviceClass
# pylint: enable=import-error # pylint: enable=import-error
@@ -21,9 +24,12 @@ DOMAINS = {
"binary_sensor": BinarySensorDeviceClass, "binary_sensor": BinarySensorDeviceClass,
"button": ButtonDeviceClass, "button": ButtonDeviceClass,
"cover": CoverDeviceClass, "cover": CoverDeviceClass,
"event": EventDeviceClass,
"number": NumberDeviceClass, "number": NumberDeviceClass,
"sensor": SensorDeviceClass, "sensor": SensorDeviceClass,
"switch": SwitchDeviceClass, "switch": SwitchDeviceClass,
"text_sensor": TextSensorDeviceClass,
"valve": ValveDeviceClass,
} }

View File

@@ -1 +1,3 @@
datetime: datetime:
time:

View File

@@ -183,3 +183,25 @@ datetime:
- x.hour - x.hour
- x.minute - x.minute
- x.second - x.second
- platform: template
name: DateTime
id: test_datetime
type: datetime
set_action:
- logger.log: "set_value"
on_value:
- logger.log:
format: "DateTime: %04d-%02d-%02d %02d:%02d:%02d"
args:
- x.year
- x.month
- x.day_of_month
- x.hour
- x.minute
- x.second
time:
- platform: sntp # Required for datetime
wifi: # Required for sntp time
ap:

View File

@@ -0,0 +1,2 @@
packages:
common: !include common.yaml

View File

@@ -0,0 +1,2 @@
packages:
common: !include common.yaml

View File

@@ -0,0 +1,2 @@
packages:
common: !include common.yaml

View File

@@ -0,0 +1,2 @@
packages:
common: !include common.yaml

View File

@@ -0,0 +1,2 @@
packages:
common: !include common.yaml

View File

@@ -0,0 +1,2 @@
packages:
common: !include common.yaml

View File

@@ -0,0 +1,2 @@
packages:
common: !include common.yaml

View File

@@ -0,0 +1,2 @@
packages:
common: !include common.yaml